Compare commits

..

22 Commits

Author SHA1 Message Date
LGUG2Z
ee76d8bc1f fix(cli): start ahk with arg list 2024-05-29 14:57:07 -07:00
LGUG2Z
458cac9ce7 fix(cli): respect named ws send behaviour
This commit ensures that the "send" behaviour is respected in
named-workspace command variants such as send-to-named-workspace.
2024-05-29 13:10:07 -07:00
LGUG2Z
6c90001c00 fix(stackbar): destroy hpen, hbrush + hfont objs
This commit ensures that HPEN, HBRUSH and HFONT objects which are used
to draw stackbar tabs are explicitly destroyed with calls to
DeleteObject after ReleaseDC has been called.

re #855
2024-05-28 17:41:19 -07:00
LGUG2Z
3232d9242a fix(borders): destroy hpen and hbrush objects
This commit ensures that HPEN and HBRUSH objects created to draw window
borders are explicitly destroyed with calls to DeleteObject after
EndPaint has been called.

re #855
2024-05-28 17:35:07 -07:00
LGUG2Z
e57b08d073 feat(wm): unset unknown bits when using bitflags
This commit switches to using the bitflags from_bits_truncate fn to
handle applications like Foxit Reader which use garbage bits that aren't
part of the Window Styles or Extended Window Styles Win32 specs.

Any unknown bits that are not in the Win32 specs will be unset when this
function is run.
2024-05-28 17:22:48 -07:00
LGUG2Z
cfb0c7f2ce feat(wm): allow stack expansion in direction
This commit allows for the user to expand container stacks while focused
on an an existing (len > 1) container stack by using the stack command
with a desired direction.

resolve #847
2024-05-25 20:34:59 -07:00
LGUG2Z
5cff90a62b fix(wm): reap monocle and maximized windows
This commit updates the orphan reaper to also reap orphan windows in the
monocle container and any tracked maximized windows that no longer
exist.
2024-05-24 15:07:37 -07:00
LGUG2Z
da7a9394d8 fix(wm): detect both physical and virtual monitors
This commit addresses a regression in v0.1.26 that was introduced by the
win32-display-data crate, where virtual monitors would not be detected
in scans by the wm.

The actual fix has been made upstream in win32-display-data:

2a0f7166da

fix #846
2024-05-24 13:21:20 -07:00
LGUG2Z
88684f991f feat(cli): add stack-all and unstack-all cmds
This commit adds two new commands, stack-all, which puts all windows in
the focused workspace into a single stack, and unstack-all, which
unstacks all windows in the currently focused container.
2024-05-24 11:13:35 -07:00
LGUG2Z
03fdbea5cd refactor(ahk): remove derive-ahk and references
This commit finally sunsets the derive-ahk proc macro and the
ahk-library cli command.

There is now a dedicated, stripped down komorebi.ahk example on the docs
website which mirrors the contents and style of the sample whkdrc:
https://lgug2z.github.io/komorebi/common-workflows/autohotkey.html
2024-05-23 22:57:27 -07:00
LGUG2Z
340c137342 fix(wm): dynamically reserve monitor ring space
This commit makes a small change to dynamically keep reserving space in
the VecDeque that backs Ring<Monitor> until an index preference can be
contained within the current length.

This commit also fixes some clippy lints and adds some allow
annotations.
2024-05-23 19:00:46 -07:00
LGUG2Z
e46a0757e3 chore(dev): begin v0.1.27-dev 2024-05-23 16:53:57 -07:00
LGUG2Z
3556f38469 chore(release): v0.1.26 2024-05-22 15:36:20 -07:00
LGUG2Z
62770033f2 fix(wm): make close + min op on foreground hwnd
This commit changes the handlers for the Close and Minimize
SocketMessages to operate on the output of
WindowsApi::foreground_window, without checking the window manager state
as it was doing previously.

This will allow the commands to operate on any kind of managed or
unmanaged window, and the appropriate WinEvent will be emitted by the
closed window for the window manager state to be updated when the
WinEvent goes through process_event.

fix #839
2024-05-22 15:33:03 -07:00
LGUG2Z
e294dbbe93 feat(cli): rename and deprecation feedback
This commit adds feedback about renamed and deprecated configuration
options in a user's komorebi.json file when the 'start' command is run.
2024-05-22 12:26:11 -07:00
LGUG2Z
47f0ab1ef3 feat(wm): handle OBJECT_NAMECHANGE for all apps
This commit ensures that EVENT_OBJECT_NAMECHANGE is handled for all
windows.

Previously this was mapped to WindowManagerEvent::Show, as this is the
event that apps like Firefox and JetBrains IDEs sent on launch instead
of EVENT_OBJECT_SHOW like normal apps.

Now that we are using EVENT_OBJECT_NAMECHANGE to update titles on
stackbar tabs, when a window which is not in the whitelist of
object_name_change_applications sends this event, it will be handled by
the new WindowManagerEvent::TitleUpdate variant.

This ensures that a stackbar_manager::Notification is sent at the end of
process_event to update stackbar tabs when application titles are
changing.

resolve #842
2024-05-22 11:37:49 -07:00
LGUG2Z
c4d62fc4f6 feat(wm): run orphan reaper in a dedicated thread
Until now the orphan window/container reaper has always run on every
WinEvent. Unfortunately Windows Terminal does not sent a WinEvent when
it is closed.

This is a problem for the new border_manager module which draws and
destroys borders based on notifications sent to it after WinEvents and
SocketMessages have been processed.

Since Windows Terminal is not sending a WinEvent on close, this means
that user interaction is required to remove the ghost border that gets
left behind.

This commit starts a separate thread for the reaper where it runs once
every second in a loop.

This is quite wasteful and definitely not something I wanted to
implement, but a temporary solution is needed given the popularity of
the buggy application in question.

An issue on the Windows Terminal tracker has been opened here:
https://github.com/microsoft/terminal/issues/17298
2024-05-21 19:27:58 -07:00
LGUG2Z
69680b4238 fix(stackbar): destroy stackbars on ws change
This commit ensures that whenever we receive a
stackbar_manager::Notification any stackbars not associated with the
current workspace on each monitor are destroyed.

fix #838
2024-05-21 08:23:05 -07:00
LGUG2Z
0dc17e9cb3 fix(wm): restore containers when closing monocle
This commit fixes a regression introduced by hiding other containers
when monocle is enabled. When the monocle container is closed, other
containers on the workspace will now be restored.

re #834
2024-05-20 08:24:55 -07:00
LGUG2Z
0f44efaa82 docs(wm): add komorebi-gui binary, update mkdocs 2024-05-19 16:37:35 -07:00
LGUG2Z
05af7ce16a feat(gui): add the komorebi-gui debug tool
This commit adds the komorebi-gui debug tool build with egui and eframe.

This tool was built from scratch in a YouTube mini-series which can be
found here: https://www.youtube.com/watch?v=zZKjBMt4kZ4

The most interesting part of this tool right now is the ability to view
debug information about each window as it goes through the rules engine.

While it's possible to change runtime configuration options with this
tool, it is not yet possible to write those changes out to the
configuration file.
2024-05-19 14:27:18 -07:00
LGUG2Z
92447723d2 fix(wm): respect horizontal focus from monocle
This commit ensures that horizontal focus moves onto other monitors from
a monocle container are respected (ie. we don't try moving left/right
within the workspace on the focused monitor).

Additionally, if the user tries to alt-tab a window to the foreground on
a workspace where a monocle container exists, the window will flash
before being hidden behind the monocle container as a visual cue that
monocle mode needs to be disabled to access that window.

This is in contrast to the current behaviour where that window floats on
top of the monocle container in a somewhat broken state.

re #834
2024-05-19 12:45:35 -07:00
46 changed files with 4394 additions and 617 deletions

View File

@@ -93,9 +93,10 @@ 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-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
target/${{ matrix.target }}/release/komorebic-no-console.pdb
target/${{ matrix.target }}/release/komorebi-gui.pdb
target/wix/komorebi-*.msi
retention-days: 7
- name: Check GoReleaser

3
.gitignore vendored
View File

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

View File

@@ -35,6 +35,15 @@ builds:
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"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"

3087
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,10 @@
resolver = "2"
members = [
"derive-ahk",
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
]

View File

@@ -1,14 +0,0 @@
[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"

View File

@@ -1,225 +0,0 @@
#![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,16 @@
# 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
```

12
docs/cli/gui.md Normal file
View File

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

View File

@@ -0,0 +1,16 @@
# 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,4 +1,4 @@
# AutoHotKey
# AutoHotkey
If you would like to use Autohotkey, please make sure you have AutoHotKey v2
installed.
@@ -10,8 +10,8 @@ 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.
```
{% include "../komorebi.ahk" %}
```autohotkey
{% include "./komorebi.ahk.txt" %}
```
By default, the `komorebi.ahk` file should be located in the `$Env:USERPROFILE`
@@ -19,4 +19,4 @@ 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`.
and then start komorebi with Autohotkey by running `komorebic start --ahk`.

View File

@@ -1,4 +1,4 @@
# Dynamically Layout Switching
# Dynamic 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

@@ -45,7 +45,7 @@ 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
## 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
@@ -114,6 +114,7 @@ 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
```
If the binaries have been built and added to your `$PATH` correctly, you should
@@ -127,3 +128,21 @@ 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` to make sure that both
the `komorebi` 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
```

71
docs/komorebi.ahk.txt Normal file
View File

@@ -0,0 +1,71 @@
#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,3 +1,5 @@
# 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.
@@ -49,8 +51,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,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.26-dev.0"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,4 +1,4 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::colour::Colour;
@@ -29,6 +29,7 @@ pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
pub use komorebi_core::StackbarLabel;
pub use komorebi_core::StackbarMode;
pub use komorebi_core::WindowKind;

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.26-dev.0"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,5 +1,5 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::use_self)]
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
use std::path::Path;
use std::path::PathBuf;
@@ -43,6 +43,8 @@ pub enum SocketMessage {
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),
StackAll,
UnstackAll,
ResizeWindowEdge(OperationDirection, Sizing),
ResizeWindowAxis(Axis, Sizing),
UnstackWindow,

14
komorebi-gui/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "komorebi-gui"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
egui_extras = { version = "0.27" }
eframe = "0.27"
komorebi-client = { path = "../komorebi-client" }
serde_json = "1"
random_word = { version = "0.4.3", features = ["en"] }
windows = { workspace = true }

807
komorebi-gui/src/main.rs Normal file
View File

@@ -0,0 +1,807 @@
#![warn(clippy::all)]
use eframe::egui;
use eframe::egui::color_picker::Alpha;
use eframe::egui::Color32;
use eframe::egui::ViewportBuilder;
use komorebi_client::BorderStyle;
use komorebi_client::Colour;
use komorebi_client::DefaultLayout;
use komorebi_client::GlobalState;
use komorebi_client::Layout;
use komorebi_client::Rect;
use komorebi_client::Rgb;
use komorebi_client::RuleDebug;
use komorebi_client::SocketMessage;
use komorebi_client::StackbarLabel;
use komorebi_client::StackbarMode;
use komorebi_client::State;
use komorebi_client::Window;
use komorebi_client::WindowKind;
use std::collections::HashMap;
use std::time::Duration;
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
fn main() {
let native_options = eframe::NativeOptions {
viewport: ViewportBuilder::default()
.with_always_on_top()
.with_inner_size([320.0, 500.0]),
follow_system_theme: true,
..Default::default()
};
let _ = eframe::run_native(
"komorebi-gui",
native_options,
Box::new(|cc| Box::new(KomorebiGui::new(cc))),
);
}
struct BorderColours {
single: Color32,
stack: Color32,
monocle: Color32,
unfocused: Color32,
}
struct BorderConfig {
border_enabled: bool,
border_colours: BorderColours,
border_style: BorderStyle,
border_offset: i32,
border_width: i32,
}
struct StackbarConfig {
mode: StackbarMode,
label: StackbarLabel,
height: i32,
width: i32,
focused_text_colour: Color32,
unfocused_text_colour: Color32,
background_colour: Color32,
}
struct MonitorConfig {
size: Rect,
work_area_offset: Rect,
workspaces: Vec<WorkspaceConfig>,
}
impl From<&komorebi_client::Monitor> for MonitorConfig {
fn from(value: &komorebi_client::Monitor) -> Self {
let mut workspaces = vec![];
for ws in value.workspaces() {
workspaces.push(WorkspaceConfig::from(ws));
}
Self {
size: *value.size(),
work_area_offset: value.work_area_offset().unwrap_or_default(),
workspaces,
}
}
}
struct WorkspaceConfig {
name: String,
tile: bool,
layout: DefaultLayout,
container_padding: i32,
workspace_padding: i32,
}
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
fn from(value: &komorebi_client::Workspace) -> Self {
let layout = match value.layout() {
Layout::Default(layout) => *layout,
Layout::Custom(_) => DefaultLayout::BSP,
};
let name = value
.name()
.to_owned()
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
Self {
layout,
name,
tile: *value.tile(),
workspace_padding: value.workspace_padding().unwrap_or(20),
container_padding: value.container_padding().unwrap_or(20),
}
}
}
struct KomorebiGui {
border_config: BorderConfig,
stackbar_config: StackbarConfig,
mouse_follows_focus: bool,
monitors: Vec<MonitorConfig>,
workspace_names: HashMap<usize, Vec<String>>,
debug_hwnd: isize,
debug_windows: Vec<Window>,
debug_rule: Option<RuleDebug>,
}
fn colour32(colour: Option<Colour>) -> Color32 {
match colour {
Some(Colour::Rgb(rgb)) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
Some(Colour::Hex(hex)) => {
let rgb = Rgb::from(hex);
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
}
None => Color32::from_rgb(0, 0, 0),
}
}
impl KomorebiGui {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let global_state: GlobalState = serde_json::from_str(
&komorebi_client::send_query(&SocketMessage::GlobalState).unwrap(),
)
.unwrap();
let state: State =
serde_json::from_str(&komorebi_client::send_query(&SocketMessage::State).unwrap())
.unwrap();
let border_colours = BorderColours {
single: colour32(global_state.border_colours.single),
stack: colour32(global_state.border_colours.stack),
monocle: colour32(global_state.border_colours.monocle),
unfocused: colour32(global_state.border_colours.unfocused),
};
let border_config = BorderConfig {
border_enabled: global_state.border_enabled,
border_colours,
border_style: global_state.border_style,
border_offset: global_state.border_offset,
border_width: global_state.border_width,
};
let mut monitors = vec![];
for m in state.monitors.elements() {
monitors.push(MonitorConfig::from(m));
}
let mut workspace_names = HashMap::new();
for (monitor_idx, m) in monitors.iter().enumerate() {
for ws in &m.workspaces {
let names = workspace_names.entry(monitor_idx).or_insert_with(Vec::new);
names.push(ws.name.clone());
}
}
let stackbar_config = StackbarConfig {
mode: global_state.stackbar_mode,
height: global_state.stackbar_height,
width: global_state.stackbar_tab_width,
label: global_state.stackbar_label,
focused_text_colour: colour32(Some(global_state.stackbar_focused_text_colour)),
unfocused_text_colour: colour32(Some(global_state.stackbar_unfocused_text_colour)),
background_colour: colour32(Some(global_state.stackbar_tab_background_colour)),
};
let mut debug_windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(&mut debug_windows as *mut Vec<Window> as isize),
)
.unwrap();
};
Self {
border_config,
mouse_follows_focus: state.mouse_follows_focus,
monitors,
workspace_names,
debug_hwnd: 0,
debug_windows,
stackbar_config,
debug_rule: None,
}
}
}
extern "system" fn enum_window(
hwnd: windows::Win32::Foundation::HWND,
lparam: windows::Win32::Foundation::LPARAM,
) -> windows::Win32::Foundation::BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let window = Window { hwnd: hwnd.0 };
if window.is_window()
&& !window.is_miminized()
&& window.is_visible()
&& window.title().is_ok()
&& window.exe().is_ok()
{
windows.push(window);
}
true.into()
}
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
let language = "json";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
}
impl eframe::App for KomorebiGui {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ctx.set_pixels_per_point(2.0);
egui::ScrollArea::vertical().show(ui, |ui| {
ui.set_width(ctx.screen_rect().width());
ui.collapsing("Debugging", |ui| {
ui.collapsing("Window Rules", |ui| {
let window = Window {
hwnd: self.debug_hwnd,
};
let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
format!("{title} ({exe})")
} else {
String::from("Select a Window")
};
if ui.button("Refresh Windows").clicked() {
let mut debug_windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(
&mut debug_windows as *mut Vec<Window> as isize,
),
)
.unwrap();
};
self.debug_windows = debug_windows;
}
egui::ComboBox::from_label("Select a Window")
.selected_text(label)
.show_ui(ui, |ui| {
for w in &self.debug_windows {
if ui
.selectable_value(
&mut self.debug_hwnd,
w.hwnd,
format!(
"{} ({})",
w.title().unwrap(),
w.exe().unwrap()
),
)
.changed()
{
let debug_rule: RuleDebug = serde_json::from_str(
&komorebi_client::send_query(
&SocketMessage::DebugWindow(self.debug_hwnd),
)
.unwrap(),
)
.unwrap();
self.debug_rule = Some(debug_rule)
}
}
});
if let Some(debug_rule) = &self.debug_rule {
json_view_ui(ui, &serde_json::to_string_pretty(debug_rule).unwrap())
}
});
});
ui.collapsing("Mouse", |ui| {
if ui
.toggle_value(&mut self.mouse_follows_focus, "Mouse Follows Focus")
.changed()
{
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
))
.unwrap();
}
});
ui.collapsing("Border", |ui| {
if ui
.toggle_value(&mut self.border_config.border_enabled, "Border")
.changed()
{
komorebi_client::send_message(&SocketMessage::Border(
self.border_config.border_enabled,
))
.unwrap();
}
ui.collapsing("Colours", |ui| {
ui.collapsing("Single", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.single,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Single,
self.border_config.border_colours.single.r() as u32,
self.border_config.border_colours.single.g() as u32,
self.border_config.border_colours.single.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Stack", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.stack,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Stack,
self.border_config.border_colours.stack.r() as u32,
self.border_config.border_colours.stack.g() as u32,
self.border_config.border_colours.stack.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Monocle", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.monocle,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Monocle,
self.border_config.border_colours.monocle.r() as u32,
self.border_config.border_colours.monocle.g() as u32,
self.border_config.border_colours.monocle.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Unfocused", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.unfocused,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Unfocused,
self.border_config.border_colours.unfocused.r() as u32,
self.border_config.border_colours.unfocused.g() as u32,
self.border_config.border_colours.unfocused.b() as u32,
))
.unwrap();
}
})
});
ui.collapsing("Style", |ui| {
for option in [
BorderStyle::System,
BorderStyle::Rounded,
BorderStyle::Square,
] {
if ui
.add(egui::SelectableLabel::new(
self.border_config.border_style == option,
option.to_string(),
))
.clicked()
{
self.border_config.border_style = option;
komorebi_client::send_message(&SocketMessage::BorderStyle(
self.border_config.border_style,
))
.unwrap();
std::thread::sleep(Duration::from_secs(1));
komorebi_client::send_message(&SocketMessage::Retile).unwrap();
}
}
});
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_width,
-50..=50,
))
.changed()
{
komorebi_client::send_message(&SocketMessage::BorderWidth(
self.border_config.border_width,
))
.unwrap();
};
});
ui.collapsing("Offset", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_offset,
-50..=50,
))
.changed()
{
komorebi_client::send_message(&SocketMessage::BorderOffset(
self.border_config.border_offset,
))
.unwrap();
};
});
});
ui.collapsing("Stackbar", |ui| {
for option in [
StackbarMode::Never,
StackbarMode::OnStack,
StackbarMode::Always,
] {
if ui
.add(egui::SelectableLabel::new(
self.stackbar_config.mode == option,
option.to_string(),
))
.clicked()
{
self.stackbar_config.mode = option;
komorebi_client::send_message(&SocketMessage::StackbarMode(
self.stackbar_config.mode,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
}
}
ui.collapsing("Label", |ui| {
for option in [StackbarLabel::Process, StackbarLabel::Title] {
if ui
.add(egui::SelectableLabel::new(
self.stackbar_config.label == option,
option.to_string(),
))
.clicked()
{
self.stackbar_config.label = option;
komorebi_client::send_message(&SocketMessage::StackbarLabel(
self.stackbar_config.label,
))
.unwrap();
}
}
});
ui.collapsing("Colours", |ui| {
ui.collapsing("Focused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.focused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarFocusedTextColour(
self.stackbar_config.focused_text_colour.r() as u32,
self.stackbar_config.focused_text_colour.g() as u32,
self.stackbar_config.focused_text_colour.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Unfocused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.unfocused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarUnfocusedTextColour(
self.stackbar_config.unfocused_text_colour.r() as u32,
self.stackbar_config.unfocused_text_colour.g() as u32,
self.stackbar_config.unfocused_text_colour.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Background", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.background_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarBackgroundColour(
self.stackbar_config.background_colour.r() as u32,
self.stackbar_config.background_colour.g() as u32,
self.stackbar_config.background_colour.b() as u32,
),
)
.unwrap();
}
})
});
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(&mut self.stackbar_config.width, 0..=500))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarTabWidth(
self.stackbar_config.width,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
};
});
ui.collapsing("Height", |ui| {
if ui
.add(egui::Slider::new(&mut self.stackbar_config.height, 0..=100))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarHeight(
self.stackbar_config.height,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
};
});
});
for (monitor_idx, monitor) in self.monitors.iter_mut().enumerate() {
ui.collapsing(
format!(
"Monitor {monitor_idx} ({}x{})",
monitor.size.right, monitor.size.bottom
),
|ui| {
ui.collapsing("Work Area Offset", |ui| {
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.left,
0..=500,
)
.text("Left"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.top,
0..=500,
)
.text("Top"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.right,
0..=500,
)
.text("Right"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.bottom,
0..=500,
)
.text("Bottom"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
});
ui.collapsing("Workspaces", |ui| {
for (workspace_idx, workspace) in
monitor.workspaces.iter_mut().enumerate()
{
ui.collapsing(
format!("Workspace {workspace_idx} ({})", workspace.name),
|ui| {
if ui.button("Focus").clicked() {
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(false),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
monitor_idx,
workspace_idx,
),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
),
)
.unwrap();
}
if ui
.toggle_value(&mut workspace.tile, "Tiling")
.changed()
{
komorebi_client::send_message(
&SocketMessage::WorkspaceTiling(
monitor_idx,
workspace_idx,
workspace.tile,
),
)
.unwrap();
}
ui.collapsing("Name", |ui| {
let monitor_workspaces = self
.workspace_names
.get_mut(&monitor_idx)
.unwrap();
let workspace_name =
&mut monitor_workspaces[workspace_idx];
if ui
.text_edit_singleline(workspace_name)
.lost_focus()
{
workspace.name.clone_from(workspace_name);
komorebi_client::send_message(
&SocketMessage::WorkspaceName(
monitor_idx,
workspace_idx,
workspace.name.clone(),
),
)
.unwrap();
}
});
ui.collapsing("Layout", |ui| {
for option in [
DefaultLayout::BSP,
DefaultLayout::Columns,
DefaultLayout::Rows,
DefaultLayout::VerticalStack,
DefaultLayout::HorizontalStack,
DefaultLayout::UltrawideVerticalStack,
DefaultLayout::Grid,
] {
if ui
.add(egui::SelectableLabel::new(
workspace.layout == option,
option.to_string(),
))
.clicked()
{
workspace.layout = option;
komorebi_client::send_message(
&SocketMessage::WorkspaceLayout(
monitor_idx,
workspace_idx,
workspace.layout,
),
)
.unwrap();
}
}
});
ui.collapsing("Container Padding", |ui| {
if ui
.add(egui::Slider::new(
&mut workspace.container_padding,
0..=100,
))
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::ContainerPadding(
monitor_idx,
workspace_idx,
workspace.container_padding,
),
)
.unwrap();
};
});
ui.collapsing("Workspace Padding", |ui| {
if ui
.add(egui::Slider::new(
&mut workspace.workspace_padding,
0..=100,
))
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::WorkspacePadding(
monitor_idx,
workspace_idx,
workspace.workspace_padding,
),
)
.unwrap();
};
});
},
);
}
});
},
);
}
});
});
}
}

View File

@@ -1,56 +0,0 @@
#SingleInstance Force
; Load library
#Include komorebic.lib.ahk
; Focus windows
!h::Focus("left")
!j::Focus("down")
!k::Focus("up")
!l::Focus("right")
!+[::CycleFocus("previous")
!+]::CycleFocus("next")
; Move windows
!+h::Move("left")
!+j::Move("down")
!+k::Move("up")
!+l::Move("right")
!+Enter::Promote()
; Stack windows
!Left::Stack("left")
!Right::Stack("right")
!Up::Stack("up")
!Down::Stack("down")
!;::Unstack()
![::CycleStack("previous")
!]::CycleStack("next")
; Resize
!=::ResizeAxis("horizontal", "increase")
!-::ResizeAxis("horizontal", "decrease")
!+=::ResizeAxis("vertical", "increase")
!+-::ResizeAxis("vertical", "decrease")
; Manipulate windows
!t::ToggleFloat()
!+f::ToggleMonocle()
; Window manager options
!+r::Retile()
!p::TogglePause()
; Layouts
!x::FlipLayout("horizontal")
!y::FlipLayout("vertical")
; Workspaces
!1::FocusWorkspace(0)
!2::FocusWorkspace(1)
!3::FocusWorkspace(2)
; Move windows across workspaces
!+1::MoveToWorkspace(0)
!+2::MoveToWorkspace(1)
!+3::MoveToWorkspace(2)

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.26-dev.0"
version = "0.1.27-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -48,7 +48,7 @@ windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.52"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data" }
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "2a0f7166da154880a1750b91829b1186d9c6a00c" }
[features]
deadlock_detection = []

View File

@@ -27,6 +27,7 @@ use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::DeleteObject;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::Rectangle;
@@ -210,6 +211,8 @@ impl Border {
}
}
EndPaint(window, &ps);
DeleteObject(hpen);
DeleteObject(hbrush);
ValidateRect(window, None);
}

View File

@@ -1,3 +1,5 @@
#![warn(clippy::all)]
pub mod border_manager;
pub mod com;
#[macro_use]
@@ -9,6 +11,7 @@ pub mod monitor_reconciliator;
pub mod process_command;
pub mod process_event;
pub mod process_movement;
pub mod reaper;
pub mod set_window_position;
pub mod stackbar_manager;
pub mod static_config;

View File

@@ -1,9 +1,10 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![warn(clippy::all)]
#![allow(
clippy::missing_errors_doc,
clippy::redundant_pub_crate,
clippy::significant_drop_tightening,
clippy::significant_drop_in_scrutinee
clippy::significant_drop_in_scrutinee,
clippy::doc_markdown
)]
use std::path::PathBuf;
@@ -30,6 +31,7 @@ use komorebi::process_command::listen_for_commands;
use komorebi::process_command::listen_for_commands_tcp;
use komorebi::process_event::listen_for_events;
use komorebi::process_movement::listen_for_movements;
use komorebi::reaper;
use komorebi::stackbar_manager;
use komorebi::static_config::StaticConfig;
use komorebi::window_manager::WindowManager;
@@ -258,6 +260,7 @@ fn main() -> Result<()> {
stackbar_manager::listen_for_notifications(wm.clone());
workspace_reconciliator::listen_for_notifications(wm.clone());
monitor_reconciliator::listen_for_notifications(wm.clone())?;
reaper::watch_for_orphans(wm.clone());
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {

View File

@@ -57,7 +57,7 @@ pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
}
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
Ok(win32_display_data::connected_displays()
Ok(win32_display_data::connected_displays_all()
.flatten()
.map(|display| {
let path = display.device_path;

View File

@@ -205,6 +205,8 @@ impl WindowManager {
}
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
SocketMessage::StackAll => self.stack_all()?,
SocketMessage::UnstackAll => self.unstack_all()?,
SocketMessage::CycleStack(direction) => {
self.cycle_container_window_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
@@ -215,8 +217,18 @@ impl WindowManager {
WindowsApi::center_cursor_in_rect(&focused_window_rect)?;
WindowsApi::left_click();
}
SocketMessage::Close => self.focused_window()?.close()?,
SocketMessage::Minimize => self.focused_window()?.minimize(),
SocketMessage::Close => {
Window {
hwnd: WindowsApi::foreground_window()?,
}
.close()?;
}
SocketMessage::Minimize => {
Window {
hwnd: WindowsApi::foreground_window()?,
}
.minimize();
}
SocketMessage::ToggleFloat => self.toggle_float()?,
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
SocketMessage::ToggleMaximize => self.toggle_maximize()?,

View File

@@ -24,6 +24,7 @@ use crate::window::RuleDebug;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::workspace_reconciliator;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
@@ -120,37 +121,11 @@ impl WindowManager {
_ => {}
}
let offset = self.work_area_offset;
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
if let WindowManagerEvent::FocusChange(_, window) = event {
let _ = workspace.focus_changed(window.hwnd);
}
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
@@ -335,7 +310,7 @@ impl WindowManager {
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let workspace_has_monocle_container = workspace.monocle_container().is_some();
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && !needs_reconciliation {
match behaviour {
@@ -353,10 +328,18 @@ impl WindowManager {
}
}
if matches!(event, WindowManagerEvent::Uncloak(_, _)) {
if workspace_contains_window && workspace_has_monocle_container {
self.toggle_monocle()?;
window.focus(self.mouse_follows_focus)?;
if workspace_contains_window {
let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container {
if let Some(monocle_window) = monocle.focused_window() {
if monocle_window.hwnd == window.hwnd {
monocle_window_event = true;
}
}
}
if !monocle_window_event && monocle_container.is_some() {
window.hide();
}
}
}
@@ -588,10 +571,9 @@ impl WindowManager {
}
}
}
WindowManagerEvent::ForceUpdate(_) => {
self.update_focused_workspace(false, true)?;
}
WindowManagerEvent::MouseCapture(..) | WindowManagerEvent::Cloak(..) => {}
WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::TitleUpdate(..) => {}
};
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
@@ -629,7 +611,16 @@ impl WindowManager {
border_manager::event_tx().send(border_manager::Notification)?;
stackbar_manager::event_tx().send(stackbar_manager::Notification)?;
tracing::info!("processed: {}", event.window().to_string());
// Too many spammy OBJECT_NAMECHANGE events from JetBrains IDEs
if !matches!(
event,
WindowManagerEvent::Show(WinEvent::ObjectNameChange, _)
) {
tracing::info!("processed: {}", event.window().to_string());
} else {
tracing::trace!("processed: {}", event.window().to_string());
}
Ok(())
}
}

66
komorebi/src/reaper.rs Normal file
View File

@@ -0,0 +1,66 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::WindowManager;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match find_orphans(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
});
}
pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("watching");
let arc = wm.clone();
loop {
std::thread::sleep(Duration::from_secs(1));
let mut wm = arc.lock();
let offset = wm.work_area_offset;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
border_manager::event_tx().send(border_manager::Notification)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
}
}

View File

@@ -136,6 +136,27 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
continue 'receiver;
}
// Destroy any stackbars not associated with the focused workspace
let container_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
let mut to_remove = vec![];
for (id, stackbar) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
{
stackbar.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
let container_padding = ws
.container_padding()
.unwrap_or_else(|| DEFAULT_CONTAINER_PADDING.load_consume());

View File

@@ -27,6 +27,7 @@ use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::CreateFontIndirectW;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::DeleteObject;
use windows::Win32::Graphics::Gdi::DrawTextW;
use windows::Win32::Graphics::Gdi::GetDC;
use windows::Win32::Graphics::Gdi::Rectangle;
@@ -235,6 +236,9 @@ impl Stackbar {
}
ReleaseDC(self.hwnd(), hdc);
DeleteObject(hpen);
DeleteObject(hbrush);
DeleteObject(hfont);
}
Ok(())

View File

@@ -229,7 +229,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.25`
/// The `komorebi.json` static configuration file reference for `v0.1.26`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -322,6 +322,48 @@ pub struct StaticConfig {
pub stackbar: Option<StackbarConfig>,
}
impl StaticConfig {
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("border", ["active_window_border"]);
map.insert("border_width", ["active_window_border_width"]);
map.insert("border_offset", ["active_window_border_offset"]);
map.insert("border_colours", ["active_window_border_colours"]);
map.insert("border_style", ["active_window_border_style"]);
let mut display = false;
for aliases in map.values() {
for a in aliases {
if raw.contains(a) {
display = true;
}
}
}
if display {
println!("\nYour 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}""#);
}
}
}
}
}
pub fn deprecated(raw: &str) {
let deprecated = ["invisible_borders"];
for option in deprecated {
if raw.contains(option) {
println!(r#""{option}" is deprecated and can be removed"#);
}
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TabsConfig {
/// Width of a stackbar tab
@@ -455,12 +497,12 @@ impl StaticConfig {
fn apply_globals(&mut self) -> Result<()> {
if let Some(monitor_index_preferences) = &self.monitor_index_preferences {
let mut preferences = MONITOR_INDEX_PREFERENCES.lock();
*preferences = monitor_index_preferences.clone();
preferences.clone_from(monitor_index_preferences);
}
if let Some(display_index_preferences) = &self.display_index_preferences {
let mut preferences = DISPLAY_INDEX_PREFERENCES.lock();
*preferences = display_index_preferences.clone();
preferences.clone_from(display_index_preferences);
}
if let Some(behaviour) = self.window_hiding_behaviour {

View File

@@ -7,7 +7,6 @@ use std::fmt::Write as _;
use std::time::Duration;
use color_eyre::eyre;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
@@ -270,12 +269,12 @@ impl Window {
pub fn style(self) -> Result<WindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
WindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
Ok(WindowStyle::from_bits_truncate(bits))
}
pub fn ex_style(self) -> Result<ExtendedWindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
ExtendedWindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
Ok(ExtendedWindowStyle::from_bits_truncate(bits))
}
pub fn title(self) -> Result<String> {

View File

@@ -37,6 +37,7 @@ use komorebi_core::OperationBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::StackbarLabel;
use komorebi_core::WindowContainerBehaviour;
use crate::border_manager;
@@ -47,6 +48,7 @@ use crate::load_configuration;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_MODE;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
@@ -122,6 +124,7 @@ pub struct GlobalState {
pub border_offset: i32,
pub border_width: i32,
pub stackbar_mode: StackbarMode,
pub stackbar_label: StackbarLabel,
pub stackbar_focused_text_colour: Colour,
pub stackbar_unfocused_text_colour: Colour,
pub stackbar_tab_background_colour: Colour,
@@ -164,6 +167,7 @@ impl Default for GlobalState {
border_offset: border_manager::BORDER_OFFSET.load(Ordering::SeqCst),
border_width: border_manager::BORDER_WIDTH.load(Ordering::SeqCst),
stackbar_mode: STACKBAR_MODE.load(),
stackbar_label: STACKBAR_LABEL.load(),
stackbar_focused_text_colour: Colour::Rgb(Rgb::from(
STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
)),
@@ -1061,14 +1065,14 @@ impl WindowManager {
target_monitor.add_container(container, workspace_idx)?;
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
}
target_monitor.load_focused_workspace(mouse_follows_focus)?;
target_monitor.update_focused_workspace(offset)?;
if follow {
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
}
target_monitor.load_focused_workspace(mouse_follows_focus)?;
target_monitor.update_focused_workspace(offset)?;
self.focus_monitor(monitor_idx)?;
}
@@ -1402,6 +1406,67 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn stack_all(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("stacking all windows on workspace");
let workspace = self.focused_workspace_mut()?;
let mut focused_hwnd = None;
if let Some(container) = workspace.focused_container() {
if let Some(window) = container.focused_window() {
focused_hwnd = Some(window.hwnd);
}
}
workspace.focus_container(workspace.containers().len() - 1);
while workspace.focused_container_idx() > 0 {
workspace.move_window_to_container(0)?;
workspace.focus_container(workspace.containers().len() - 1);
}
if let Some(hwnd) = focused_hwnd {
workspace.focus_container_by_window(hwnd)?;
}
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn unstack_all(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("unstacking all windows in container");
let workspace = self.focused_workspace_mut()?;
let mut focused_hwnd = None;
if let Some(container) = workspace.focused_container() {
if let Some(window) = container.focused_window() {
focused_hwnd = Some(window.hwnd);
}
}
let initial_focused_container_index = workspace.focused_container_idx();
let mut focused_container = workspace.focused_container().cloned();
while let Some(focused) = &focused_container {
if focused.windows().len() > 1 {
workspace.new_container_for_focused_window()?;
workspace.focus_container(initial_focused_container_index);
focused_container = workspace.focused_container().cloned();
} else {
focused_container = None;
}
}
if let Some(hwnd) = focused_hwnd {
workspace.focus_container_by_window(hwnd)?;
}
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn add_window_to_container(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -1438,7 +1503,15 @@ impl WindowManager {
new_idx
};
workspace.move_window_to_container(adjusted_new_index)?;
if let Some(current) = workspace.focused_container() {
if current.windows().len() > 1 {
workspace.focus_container(adjusted_new_index);
workspace.move_window_to_container(current_container_idx)?;
} else {
workspace.move_window_to_container(adjusted_new_index)?;
}
}
self.update_focused_workspace(self.mouse_follows_focus, false)?;
}

View File

@@ -27,7 +27,7 @@ pub enum WindowManagerEvent {
Manage(Window),
Unmanage(Window),
Raise(Window),
ForceUpdate(Window),
TitleUpdate(WinEvent, Window),
}
impl Display for WindowManagerEvent {
@@ -75,8 +75,8 @@ impl Display for WindowManagerEvent {
Self::Raise(window) => {
write!(f, "Raise (Window: {window})")
}
Self::ForceUpdate(window) => {
write!(f, "ForceUpdate (Window: {window})")
Self::TitleUpdate(winevent, window) => {
write!(f, "TitleUpdate (WinEvent: {winevent}, Window: {window})")
}
}
}
@@ -98,7 +98,7 @@ impl WindowManagerEvent {
| Self::Raise(window)
| Self::Manage(window)
| Self::Unmanage(window)
| Self::ForceUpdate(window) => window,
| Self::TitleUpdate(_, window) => window,
}
}
@@ -141,7 +141,7 @@ impl WindowManagerEvent {
let class = &window.class().ok()?;
let path = &window.path().ok()?;
let should_trigger = should_act(
let should_trigger_show = should_act(
title,
exe_name,
class,
@@ -151,10 +151,10 @@ impl WindowManagerEvent {
)
.is_some();
if should_trigger {
if should_trigger_show {
Option::from(Self::Show(winevent, window))
} else {
None
Option::from(Self::TitleUpdate(winevent, window))
}
}
_ => None,

View File

@@ -220,7 +220,7 @@ impl WindowsApi {
}
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
Ok(win32_display_data::connected_displays()
Ok(win32_display_data::connected_displays_all()
.flatten()
.map(|d| {
let name = d.device_name.trim_start_matches(r"\\.\").to_string();
@@ -232,7 +232,7 @@ impl WindowsApi {
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
'read: for display in win32_display_data::connected_displays().flatten() {
'read: for display in win32_display_data::connected_displays_all().flatten() {
let path = display.device_path.clone();
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
@@ -276,8 +276,7 @@ impl WindowsApi {
if monitors.elements().is_empty() {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
let current_len = monitors.elements().len();
if *preference > current_len {
while *preference > monitors.elements().len() {
monitors.elements_mut().reserve(1);
}
@@ -791,7 +790,7 @@ impl WindowsApi {
}
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
for display in win32_display_data::connected_displays().flatten() {
for display in win32_display_data::connected_displays_all().flatten() {
if display.hmonitor == hmonitor {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();

View File

@@ -80,8 +80,19 @@ pub extern "system" fn win_event_hook(
Ok(event) => event,
Err(_) => return,
};
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => return,
None => {
tracing::trace!(
"Unhandled WinEvent: {winevent} (hwnd: {}, exe: {}, title: {}, class: {})",
window.hwnd,
window.exe().unwrap_or_default(),
window.title().unwrap_or_default(),
window.class().unwrap_or_default()
);
return;
}
Some(event) => event,
};

View File

@@ -357,7 +357,7 @@ impl Workspace {
layout.bottom -= total_height;
}
window.set_position(&layout, false)?;
window.set_position(layout, false)?;
}
}
@@ -397,6 +397,28 @@ impl Workspace {
pub fn reap_orphans(&mut self) -> Result<(usize, usize)> {
let mut hwnds = vec![];
let mut floating_hwnds = vec![];
let mut remove_monocle = false;
let mut remove_maximized = false;
if let Some(monocle) = &self.monocle_container {
let window_count = monocle.windows().len();
let mut orphan_count = 0;
for window in monocle.windows() {
if !window.is_window() {
hwnds.push(window.hwnd);
orphan_count += 1;
}
}
remove_monocle = orphan_count == window_count;
}
if let Some(window) = &self.maximized_window {
if !window.is_window() {
hwnds.push(window.hwnd);
remove_maximized = true;
}
}
for window in self.visible_windows_mut().into_iter().flatten() {
if !window.is_window() {
@@ -431,6 +453,14 @@ impl Workspace {
self.containers_mut()
.retain(|c| !container_ids.contains(c.id()));
if remove_monocle {
self.set_monocle_container(None);
}
if remove_maximized {
self.set_maximized_window(None);
}
Ok((hwnds.len() + floating_hwnds.len(), container_ids.len()))
}
@@ -660,6 +690,10 @@ impl Workspace {
self.set_monocle_container_restore_idx(None);
}
for c in self.containers() {
c.restore();
}
return Ok(());
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.26-dev.0"
version = "0.1.27-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.26-dev.0"
version = "0.1.27-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]
@@ -11,7 +11,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
derive-ahk = { path = "../derive-ahk" }
komorebi-core = { path = "../komorebi-core" }
komorebi-client = { path = "../komorebi-client" }
@@ -21,7 +20,6 @@ color-eyre = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
fs-tail = "0.1"
heck = "0.5"
lazy_static = "1"
miette = { version = "7", features = ["fancy"] }
paste = "1"

View File

@@ -1,5 +1,5 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::doc_markdown)]
use chrono::Local;
use std::fs::File;
@@ -24,7 +24,6 @@ use color_eyre::eyre::bail;
use color_eyre::Result;
use dirs::data_local_dir;
use fs_tail::TailedFile;
use heck::ToKebabCase;
use komorebi_core::resolve_home_path;
use lazy_static::lazy_static;
use miette::NamedSource;
@@ -39,8 +38,7 @@ use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use derive_ahk::AhkFunction;
use derive_ahk::AhkLibrary;
use komorebi_client::StaticConfig;
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
@@ -101,14 +99,6 @@ lazy_static! {
};
}
trait AhkLibrary {
fn generate_ahk_library() -> String;
}
trait AhkFunction {
fn generate_ahk_function() -> String;
}
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("{message}")]
#[diagnostic(code(komorebi::configuration), help("try fixing this syntax error"))]
@@ -140,7 +130,7 @@ macro_rules! gen_enum_subcommand_args {
( $( $name:ident: $element:ty ),+ $(,)? ) => {
$(
paste! {
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
#[clap(value_enum)]
[<$element:snake>]: $element
@@ -180,7 +170,7 @@ macro_rules! gen_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
/// Target index (zero-indexed)
target: usize,
@@ -205,7 +195,7 @@ macro_rules! gen_named_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
/// Target workspace name
workspace: String,
@@ -229,7 +219,7 @@ macro_rules! gen_workspace_subcommand_args {
( $( $name:ident: $(#[enum] $(@$value_enum:tt)?)? $value:ty ),+ $(,)? ) => (
paste! {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct [<Workspace $name>] {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -261,7 +251,7 @@ macro_rules! gen_named_workspace_subcommand_args {
( $( $name:ident: $(#[enum] $(@$value_enum:tt)?)? $value:ty ),+ $(,)? ) => (
paste! {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct [<NamedWorkspace $name>] {
/// Target workspace name
workspace: String,
@@ -283,7 +273,7 @@ gen_named_workspace_subcommand_args! {
Tiling: #[enum] BooleanState,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct ClearWorkspaceLayoutRules {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -292,7 +282,7 @@ pub struct ClearWorkspaceLayoutRules {
workspace: usize,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct WorkspaceCustomLayout {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -304,7 +294,7 @@ pub struct WorkspaceCustomLayout {
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct NamedWorkspaceCustomLayout {
/// Target workspace name
workspace: String,
@@ -313,7 +303,7 @@ pub struct NamedWorkspaceCustomLayout {
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct WorkspaceLayoutRule {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -328,7 +318,7 @@ pub struct WorkspaceLayoutRule {
layout: DefaultLayout,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct NamedWorkspaceLayoutRule {
/// Target workspace name
workspace: String,
@@ -340,7 +330,7 @@ pub struct NamedWorkspaceLayoutRule {
layout: DefaultLayout,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct WorkspaceCustomLayoutRule {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -355,7 +345,7 @@ pub struct WorkspaceCustomLayoutRule {
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct NamedWorkspaceCustomLayoutRule {
/// Target workspace name
workspace: String,
@@ -367,7 +357,7 @@ pub struct NamedWorkspaceCustomLayoutRule {
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct Resize {
#[clap(value_enum)]
edge: OperationDirection,
@@ -375,7 +365,7 @@ struct Resize {
sizing: Sizing,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct ResizeAxis {
#[clap(value_enum)]
axis: Axis,
@@ -383,13 +373,13 @@ struct ResizeAxis {
sizing: Sizing,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct ResizeDelta {
/// The delta of pixels by which to increase or decrease window dimensions when resizing
pixels: i32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct InvisibleBorders {
/// Size of the left invisible border
left: i32,
@@ -401,7 +391,7 @@ struct InvisibleBorders {
bottom: i32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct GlobalWorkAreaOffset {
/// Size of the left work area offset (set right to left * 2 to maintain right padding)
left: i32,
@@ -413,7 +403,7 @@ struct GlobalWorkAreaOffset {
bottom: i32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct MonitorWorkAreaOffset {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -427,7 +417,7 @@ struct MonitorWorkAreaOffset {
bottom: i32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct MonitorIndexPreference {
/// Preferred monitor index (zero-indexed)
index_preference: usize,
@@ -441,7 +431,7 @@ struct MonitorIndexPreference {
bottom: i32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct DisplayIndexPreference {
/// Preferred monitor index (zero-indexed)
index_preference: usize,
@@ -449,7 +439,7 @@ struct DisplayIndexPreference {
display: String,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct EnsureWorkspaces {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -457,7 +447,7 @@ struct EnsureWorkspaces {
workspace_count: usize,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct EnsureNamedWorkspaces {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -465,7 +455,7 @@ struct EnsureNamedWorkspaces {
names: Vec<String>,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct FocusMonitorWorkspace {
/// Target monitor index (zero-indexed)
target_monitor: usize,
@@ -473,7 +463,7 @@ struct FocusMonitorWorkspace {
target_workspace: usize,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct SendToMonitorWorkspace {
/// Target monitor index (zero-indexed)
target_monitor: usize,
@@ -481,7 +471,7 @@ pub struct SendToMonitorWorkspace {
target_workspace: usize,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
pub struct MoveToMonitorWorkspace {
/// Target monitor index (zero-indexed)
target_monitor: usize,
@@ -493,7 +483,7 @@ macro_rules! gen_focused_workspace_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
/// Pixels size to set as an integer
size: i32,
@@ -511,7 +501,7 @@ macro_rules! gen_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -533,7 +523,7 @@ macro_rules! gen_named_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
/// Target workspace name
workspace: String,
@@ -554,7 +544,7 @@ macro_rules! gen_padding_adjustment_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
#[clap(value_enum)]
sizing: Sizing,
@@ -574,7 +564,7 @@ macro_rules! gen_application_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
#[derive(clap::Parser)]
pub struct $name {
#[clap(value_enum)]
identifier: ApplicationIdentifier,
@@ -595,7 +585,7 @@ gen_application_target_subcommand_args! {
RemoveTitleBar,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct InitialWorkspaceRule {
#[clap(value_enum)]
identifier: ApplicationIdentifier,
@@ -607,7 +597,7 @@ struct InitialWorkspaceRule {
workspace: usize,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct InitialNamedWorkspaceRule {
#[clap(value_enum)]
identifier: ApplicationIdentifier,
@@ -617,7 +607,7 @@ struct InitialNamedWorkspaceRule {
workspace: String,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct WorkspaceRule {
#[clap(value_enum)]
identifier: ApplicationIdentifier,
@@ -629,7 +619,7 @@ struct WorkspaceRule {
workspace: usize,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct NamedWorkspaceRule {
#[clap(value_enum)]
identifier: ApplicationIdentifier,
@@ -639,13 +629,13 @@ struct NamedWorkspaceRule {
workspace: String,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct ToggleFocusFollowsMouse {
#[clap(value_enum, short, long, default_value = "windows")]
implementation: FocusFollowsMouseImplementation,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct FocusFollowsMouse {
#[clap(value_enum, short, long, default_value = "windows")]
implementation: FocusFollowsMouseImplementation,
@@ -653,13 +643,13 @@ struct FocusFollowsMouse {
boolean_state: BooleanState,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct Border {
#[clap(value_enum)]
boolean_state: BooleanState,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct BorderColour {
#[clap(value_enum, short, long, default_value = "single")]
window_kind: WindowKind,
@@ -671,19 +661,19 @@ struct BorderColour {
b: u32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct BorderWidth {
/// Desired width of the window border
width: i32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct BorderOffset {
/// Desired offset of the window border
offset: i32,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
#[allow(clippy::struct_excessive_bools)]
struct Start {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
@@ -706,56 +696,56 @@ struct Start {
ahk: bool,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct Stop {
/// Stop whkd if it is running as a background process
#[clap(long)]
whkd: bool,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct SaveResize {
/// File to which the resize layout dimensions should be saved
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct LoadResize {
/// File from which the resize layout dimensions should be loaded
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct LoadCustomLayout {
/// JSON or YAML file from which the custom layout definition should be loaded
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct SubscribeSocket {
/// Name of the socket to send event notifications to
socket: String,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct UnsubscribeSocket {
/// Name of the socket to stop sending event notifications to
socket: String,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct SubscribePipe {
/// Name of the pipe to send event notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct UnsubscribePipe {
/// Name of the pipe to stop sending event notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct AhkAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
path: PathBuf,
@@ -763,7 +753,7 @@ struct AhkAppSpecificConfiguration {
override_path: Option<PathBuf>,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct PwshAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
path: PathBuf,
@@ -771,19 +761,19 @@ struct PwshAppSpecificConfiguration {
override_path: Option<PathBuf>,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct FormatAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
path: PathBuf,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct AltFocusHack {
#[clap(value_enum)]
boolean_state: BooleanState,
}
#[derive(Parser, AhkFunction)]
#[derive(Parser)]
struct EnableAutostart {
/// Path to a static configuration JSON file
#[clap(action, short, long)]
@@ -806,7 +796,7 @@ struct Opts {
subcmd: SubCommand,
}
#[derive(Parser, AhkLibrary)]
#[derive(Parser)]
enum SubCommand {
#[clap(hide = true)]
Docgen,
@@ -828,6 +818,8 @@ enum SubCommand {
State,
/// Show a JSON representation of the current global state
GlobalState,
/// Launch the komorebi-gui debugging tool
Gui,
/// Show a JSON representation of visible windows
VisibleWindows,
/// Query the current window manager state
@@ -884,6 +876,10 @@ enum SubCommand {
/// Stack the focused window in the specified direction
#[clap(arg_required_else_help = true)]
Stack(Stack),
/// Stack all windows on the focused workspace
StackAll,
/// Unstack all windows in the focused container
UnstackAll,
/// Resize the focused window in the specified direction
#[clap(arg_required_else_help = true)]
#[clap(alias = "resize")]
@@ -1176,8 +1172,6 @@ enum SubCommand {
MouseFollowsFocus(MouseFollowsFocus),
/// Toggle mouse follows focus on all workspaces
ToggleMouseFollowsFocus,
/// Generate a library of AutoHotKey helper functions
AhkLibrary,
/// Generate common app-specific configurations and fixes to use in komorebi.ahk
#[clap(arg_required_else_help = true)]
#[clap(alias = "ahk-asc")]
@@ -1461,35 +1455,6 @@ fn main() -> Result<()> {
println!("{}", whkdrc.display());
}
}
SubCommand::AhkLibrary => {
let library = HOME_DIR.join("komorebic.lib.ahk");
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(library.clone())?;
let output: String = SubCommand::generate_ahk_library();
let fixed_id = output.replace("%id%", "\"%id%\"");
let fixed_stop_def = fixed_id.replace("Stop(whkd)", "Stop()");
let fixed_output =
fixed_stop_def.replace("komorebic.exe stop --whkd %whkd%", "komorebic.exe stop");
file.write_all(fixed_output.as_bytes())?;
println!(
"\nAHKv1 helper library for komorebic written to {}",
library.to_string_lossy()
);
println!("\nYou can convert this file to AHKv2 syntax using https://github.com/mmikeww/AHK-v2-script-converter");
println!(
"\nYou can include the converted library at the top of your komorebi.ahk config with this line:"
);
println!("\n#Include komorebic.lib.ahk");
}
SubCommand::Log => {
let timestamp = Local::now().format("%Y-%m-%d").to_string();
let color_log = std::env::temp_dir().join(format!("komorebi.log.{timestamp}"));
@@ -1930,7 +1895,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
let script = format!(
r#"
Start-Process '{ahk}' '{config}' -WindowStyle hidden
Start-Process '{ahk}' -ArgumentList '{config}' -WindowStyle hidden
"#,
config = config_ahk.display()
);
@@ -1952,6 +1917,25 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
);
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
let static_config = arg.config.map_or_else(
|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
if komorebi_json.is_file() {
Option::from(komorebi_json)
} else {
None
}
},
Option::from,
);
if let Some(config) = static_config {
let path = resolve_home_path(config)?;
let raw = std::fs::read_to_string(path)?;
StaticConfig::aliases(&raw);
StaticConfig::deprecated(&raw);
}
}
SubCommand::Stop(arg) => {
if arg.whkd {
@@ -2036,9 +2020,15 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
SubCommand::Stack(arg) => {
send_message(&SocketMessage::StackWindow(arg.operation_direction).as_bytes()?)?;
}
SubCommand::StackAll => {
send_message(&SocketMessage::StackAll.as_bytes()?)?;
}
SubCommand::Unstack => {
send_message(&SocketMessage::UnstackWindow.as_bytes()?)?;
}
SubCommand::UnstackAll => {
send_message(&SocketMessage::UnstackAll.as_bytes()?)?;
}
SubCommand::CycleStack(arg) => {
send_message(&SocketMessage::CycleStack(arg.cycle_direction).as_bytes()?)?;
}
@@ -2130,6 +2120,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
SubCommand::GlobalState => {
print_query(&SocketMessage::GlobalState.as_bytes()?);
}
SubCommand::Gui => {
Command::new("komorebi-gui").spawn()?;
}
SubCommand::VisibleWindows => {
print_query(&SocketMessage::VisibleWindows.as_bytes()?);
}

View File

@@ -57,6 +57,7 @@ nav:
- Troubleshooting: troubleshooting.md
- Common workflows:
- common-workflows/komorebi-config-home.md
- common-workflows/autohotkey.md
- common-workflows/borders.md
- common-workflows/stackbar.md
- common-workflows/remove-gaps.md
@@ -67,7 +68,6 @@ nav:
- common-workflows/mouse-follows-focus.md
- common-workflows/custom-layouts.md
- common-workflows/dynamic-layout-switching.md
- common-workflows/autohotkey.md
- Release notes:
- release/v0-1-22.md
- Configuration reference: https://komorebi.lgug2z.com/schema
@@ -80,6 +80,7 @@ nav:
- cli/whkdrc.md
- cli/state.md
- cli/global-state.md
- cli/gui.md
- cli/visible-windows.md
- cli/query.md
- cli/subscribe-socket.md
@@ -124,6 +125,7 @@ nav:
- cli/cycle-monitor.md
- cli/cycle-workspace.md
- cli/move-workspace-to-monitor.md
- cli/cycle-move-workspace-to-monitor.md
- cli/swap-workspaces-with-monitor.md
- cli/new-workspace.md
- cli/resize-delta.md
@@ -140,6 +142,7 @@ nav:
- cli/flip-layout.md
- cli/promote.md
- cli/promote-focus.md
- cli/promote-window.md
- cli/retile.md
- cli/monitor-index-preference.md
- cli/display-index-preference.md

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.25`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.26`",
"type": "object",
"properties": {
"app_specific_configuration_path": {
@@ -1039,6 +1039,14 @@
"type": "integer",
"format": "int32"
},
"label": {
"description": "Stackbar height",
"type": "string",
"enum": [
"Process",
"Title"
]
},
"mode": {
"description": "Stackbar mode",
"type": "string",

Binary file not shown.

View File

@@ -95,6 +95,9 @@
<Component Id='binary2' Guid='*'>
<File Id='exe2' Name='komorebic-no-console.exe' DiskId='1' Source='$(var.CargoTargetBinDir)\komorebic-no-console.exe' KeyPath='yes' />
</Component>
<Component Id='binary3' Guid='*'>
<File Id='exe3' Name='komorebi-gui.exe' DiskId='1' Source='$(var.CargoTargetBinDir)\komorebi-gui.exe' KeyPath='yes' />
</Component>
</Directory>
</Directory>
</Directory>
@@ -113,6 +116,8 @@
<ComponentRef Id='binary2' />
<ComponentRef Id='binary3' />
<Feature Id='Environment' Title='PATH Environment Variable' Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.' Level='1' Absent='allow'>
<ComponentRef Id='Path' />
</Feature>