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
106 changed files with 3167 additions and 11247 deletions

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,39 +93,17 @@ 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/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.28
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')

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@
/target
CHANGELOG.md
dummy.go
komorebic/applications.yaml
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,28 +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"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
format: zip
files:
- LICENSE.md
- LICENSE
- CHANGELOG.md
checksum:

3537
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,26 +2,21 @@
resolver = "2"
members = [
"derive-ahk",
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
]
[workspace.dependencies]
color-eyre = "0.6"
dirs = "5"
dunce = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
sysinfo = "0.30"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "2a0f7166da154880a1750b91829b1186d9c6a00c" }
windows-implement = { version = "0.53" }
windows-interface = { version = "0.53" }
shadow-rs = "0.29"
windows-implement = { version = "0.53" }
dunce = "1"
dirs = "5"
color-eyre = "0.6"
serde_json = { package = "serde_json_lenient", version = "0.1" }
[workspace.dependencies.windows]
version = "0.54"
@@ -41,6 +36,5 @@ features = [
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming"
"Win32_System_SystemServices"
]

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.
@@ -84,28 +84,8 @@ using `scoop`, `winget` or building from source.
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/H9-_c1egQ4g/hqdefault.jpg)](https://www.youtube.com/watch?v=H9-_c1egQ4g)
# 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)
# Demonstrations
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28-dev.0` 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
@@ -358,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.25"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.24"}
use anyhow::Result;
use komorebi_client::Notification;

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 @@
# transparency
# active-window-border
```
Enable or disable transparency for unfocused windows
Enable or disable the active window border
Usage: komorebic.exe transparency <BOOLEAN_STATE>
Usage: komorebic.exe active-window-border <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>

View File

@@ -3,7 +3,7 @@
```
Generate a library of AutoHotKey helper functions
Usage: komorebic.exe komorebic.exe ahk-library
Usage: komorebic.exe ahk-library
Options:
-h, --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,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,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

@@ -1,16 +0,0 @@
# border
```
Enable or disable borders
Usage: komorebic.exe border <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
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

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

@@ -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,12 +0,0 @@
# stack-all
```
Stack all windows on the focused workspace
Usage: komorebic.exe stack-all
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,12 +0,0 @@
# unstack-all
```
Unstack all windows in the focused container
Usage: komorebic.exe unstack-all
Options:
-h, --help
Print 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,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

@@ -101,16 +101,6 @@ monocle.
+-------+-----+
```
#### RightMainVerticalStack
```
+-----+-------+
| | |
+-----+ |
| | |
+-----+-------+
```
#### Horizontal Stack
```
@@ -132,7 +122,6 @@ monocle.
```
#### Rows
If you have a vertical monitor, I recommend using this layout.
```
@@ -144,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.
```
@@ -158,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!
```
@@ -196,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

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.
@@ -27,17 +27,17 @@ to manipulate the window manager, you use
`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`.
@@ -45,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
@@ -114,7 +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
```
If the binaries have been built and added to your `$PATH` correctly, you should
@@ -128,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` 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
```

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,28 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.25/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,
"border_colours": {
"active_window_border": false,
"active_window_border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399",
"unfocused": "#808080"
},
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300,
"focused_text": "#00a542",
"unfocused_text": "#b3b3b3",
"background": "#141414"
}
"monocle": "#ff3399"
},
"monitors": [
{
@@ -46,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,12 +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
run:
just install-target komorebic
cargo +stable run --bin komorebi --locked
warn $RUST_LOG="warn":
@@ -37,6 +44,7 @@ trace $RUST_LOG="trace":
just run
deadlock $RUST_LOG="trace":
just install-komorebic
cargo +stable run --bin komorebi --locked --features deadlock_detection
docgen:

View File

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

View File

@@ -1,26 +1,17 @@
#![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::container::Container;
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::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::TabsConfig;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::BorderStyle;
pub use komorebi_core::CustomLayout;
pub use komorebi_core::CycleDirection;
pub use komorebi_core::DefaultLayout;
@@ -29,9 +20,6 @@ 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;
use komorebi::DATA_DIR;

View File

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

View File

@@ -1,42 +0,0 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum AnimationStyle {
Linear,
EaseInSine,
EaseOutSine,
EaseInOutSine,
EaseInQuad,
EaseOutQuad,
EaseInOutQuad,
EaseInCubic,
EaseInOutCubic,
EaseInQuart,
EaseOutQuart,
EaseInOutQuart,
EaseInQuint,
EaseOutQuint,
EaseInOutQuint,
EaseInExpo,
EaseOutExpo,
EaseInOutExpo,
EaseInCirc,
EaseOutCirc,
EaseInOutCirc,
EaseInBack,
EaseOutBack,
EaseInOutBack,
EaseInElastic,
EaseOutElastic,
EaseInOutElastic,
EaseInBounce,
EaseOutBounce,
EaseInOutBounce,
}

View File

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

@@ -14,7 +14,7 @@ use serde::Serialize;
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

@@ -10,18 +10,9 @@ 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,7 +21,6 @@ pub enum DefaultLayout {
HorizontalStack,
UltrawideVerticalStack,
Grid,
RightMainVerticalStack,
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
}
@@ -45,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;
};
@@ -156,8 +137,7 @@ 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,
}
}
@@ -170,8 +150,7 @@ impl DefaultLayout {
Self::VerticalStack => Self::Rows,
Self::Rows => Self::Columns,
Self::Columns => Self::Grid,
Self::Grid => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::BSP,
Self::Grid => Self::BSP,
}
}
}

View File

@@ -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,

View File

@@ -7,7 +7,7 @@ use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Direction;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum Layout {
Default(DefaultLayout),
Custom(CustomLayout),

View File

@@ -1,5 +1,5 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::use_self)]
use std::path::Path;
use std::path::PathBuf;
@@ -14,7 +14,6 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
pub use custom_layout::CustomLayout;
@@ -25,7 +24,6 @@ pub use layout::Layout;
pub use operation_direction::OperationDirection;
pub use rect::Rect;
pub mod animation;
pub mod arrangement;
pub mod config_generation;
pub mod custom_layout;
@@ -45,8 +43,6 @@ pub enum SocketMessage {
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),
StackAll,
UnstackAll,
ResizeWindowEdge(OperationDirection, Sizing),
ResizeWindowAxis(Axis, Sizing),
UnstackWindow,
@@ -61,9 +57,7 @@ pub enum SocketMessage {
SendContainerToWorkspaceNumber(usize),
CycleSendContainerToWorkspace(CycleDirection),
SendContainerToMonitorWorkspaceNumber(usize, usize),
MoveContainerToMonitorWorkspaceNumber(usize, usize),
SendContainerToNamedWorkspace(String),
CycleMoveWorkspaceToMonitor(CycleDirection),
MoveWorkspaceToMonitorNumber(usize),
SwapWorkspacesToMonitorNumber(usize),
ForceFocus,
@@ -71,7 +65,6 @@ pub enum SocketMessage {
Minimize,
Promote,
PromoteFocus,
PromoteWindow(OperationDirection),
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
@@ -136,31 +129,11 @@ pub enum SocketMessage {
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
Animation(bool),
AnimationDuration(u64),
AnimationFps(u64),
AnimationStyle(AnimationStyle),
#[serde(alias = "ActiveWindowBorder")]
Border(bool),
#[serde(alias = "ActiveWindowBorderColour")]
BorderColour(WindowKind, u32, u32, u32),
#[serde(alias = "ActiveWindowBorderStyle")]
BorderStyle(BorderStyle),
BorderWidth(i32),
BorderOffset(i32),
BorderImplementation(BorderImplementation),
Transparency(bool),
TransparencyAlpha(u8),
ActiveWindowBorder(bool),
ActiveWindowBorderColour(WindowKind, u32, u32, u32),
ActiveWindowBorderWidth(i32),
ActiveWindowBorderOffset(i32),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
StackbarLabel(StackbarLabel),
StackbarFocusedTextColour(u32, u32, u32),
StackbarUnfocusedTextColour(u32, u32, u32),
StackbarBackgroundColour(u32, u32, u32),
StackbarHeight(i32),
StackbarTabWidth(i32),
StackbarFontSize(i32),
StackbarFontFamily(Option<String>),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
ResizeDelta(i32),
@@ -168,9 +141,6 @@ pub enum SocketMessage {
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
NamedWorkspaceRule(ApplicationIdentifier, String, String),
ClearWorkspaceRules(usize, usize),
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
FloatRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
@@ -178,9 +148,7 @@ pub enum SocketMessage {
IdentifyLayeredApplication(ApplicationIdentifier, String),
IdentifyBorderOverflowApplication(ApplicationIdentifier, String),
State,
GlobalState,
VisibleWindows,
MonitorInformation,
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
@@ -197,7 +165,6 @@ pub enum SocketMessage {
SocketSchema,
StaticConfigSchema,
GenerateStaticConfig,
DebugWindow(isize),
}
impl SocketMessage {
@@ -214,88 +181,20 @@ impl FromStr for SocketMessage {
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
Never,
OnStack,
}
#[derive(
Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
pub enum StackbarLabel {
#[default]
Process,
Title,
}
#[derive(
Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
)]
pub enum BorderStyle {
#[default]
/// Use the system border style
System,
/// Use the Windows 11-style rounded borders
Rounded,
/// Use the Windows 10-style square borders
Square,
}
#[derive(
Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
)]
pub enum BorderImplementation {
#[default]
/// Use the adjustable komorebi border implementation
Komorebi,
/// Use the thin Windows accent border implementation
Windows,
}
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum WindowKind {
Single,
Stack,
Monocle,
Unfocused,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum StateQuery {
FocusedMonitorIndex,
FocusedWorkspaceIndex,
@@ -316,6 +215,7 @@ pub enum StateQuery {
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum ApplicationIdentifier {
#[serde(alias = "exe")]
Exe,
@@ -330,6 +230,7 @@ pub enum ApplicationIdentifier {
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
Komorebi,
@@ -340,6 +241,7 @@ pub enum FocusFollowsMouseImplementation {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum WindowContainerBehaviour {
/// Create a new container for each new window
Create,
@@ -350,30 +252,31 @@ pub enum WindowContainerBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
Swap,
/// Insert the window container into the focused workspace on the adjacent monitor
Insert,
/// Do nothing if trying to move a window container in the direction of an adjacent monitor
NoOp,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum HidingBehaviour {
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
Hide,
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
Minimize,
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
Cloak,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows
Op,
@@ -384,6 +287,7 @@ pub enum OperationBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum Sizing {
Increase,
Decrease,

View File

@@ -13,6 +13,7 @@ use crate::Axis;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum OperationDirection {
Left,
Right,

View File

@@ -84,14 +84,4 @@ impl Rect {
bottom: (self.bottom * rect_dpi) / system_dpi,
}
}
#[must_use]
pub const fn rect(&self) -> RECT {
RECT {
left: self.left,
top: self.top,
right: self.left + self.right,
bottom: self.top + self.bottom,
}
}
}

View File

@@ -1,14 +0,0 @@
[package]
name = "komorebi-gui"
version = "0.1.28-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.28" }
eframe = "0.28"
komorebi-client = { path = "../komorebi-client" }
serde_json = "1"
random_word = { version = "0.4.3", features = ["en"] }
windows = { workspace = true }

View File

@@ -1,805 +0,0 @@
#![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| Ok(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::from(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::from(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();
};
});
},
);
}
});
},
);
}
});
});
}
}

56
komorebi.sample.ahk Normal file
View File

@@ -0,0 +1,56 @@
#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.28-dev.0"
version = "0.1.25-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -13,45 +13,40 @@ edition = "2021"
[dependencies]
komorebi-core = { path = "../komorebi-core" }
bitflags = { version = "2", features = ["serde"] }
bitflags = "2"
clap = { version = "4", features = ["derive"] }
color-eyre = { workspace = true }
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
ctrlc = { version = "3", features = ["termination"] }
ctrlc = "3"
dirs = { workspace = true }
getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
hotwatch = "0.5"
lazy_static = "1"
miow = "0.6"
miow = "0.5"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.8"
parking_lot = "0.12"
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
paste = "1"
regex = "1"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
strum = { version = "0.26", features = ["derive"] }
sysinfo = { workspace = true }
sysinfo = "0.30"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uds_windows = "1"
which = "6"
widestring = "1"
win32-display-data = { workspace = true }
windows = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.52"
shadow-rs = { workspace = true }
[build-dependencies]
shadow-rs = { workspace = true }
[features]
deadlock_detection = ["parking_lot/deadlock_detection"]
deadlock_detection = []

View File

@@ -1,3 +0,0 @@
fn main() {
shadow_rs::new().unwrap();
}

View File

@@ -1,503 +0,0 @@
use color_eyre::Result;
use komorebi_core::AnimationStyle;
use komorebi_core::Rect;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::f64::consts::PI;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_MANAGER;
use crate::ANIMATION_STYLE;
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
pub trait Ease {
fn evaluate(t: f64) -> f64;
}
pub struct Linear;
impl Ease for Linear {
fn evaluate(t: f64) -> f64 {
t
}
}
pub struct EaseInSine;
impl Ease for EaseInSine {
fn evaluate(t: f64) -> f64 {
1.0 - f64::cos((t * PI) / 2.0)
}
}
pub struct EaseOutSine;
impl Ease for EaseOutSine {
fn evaluate(t: f64) -> f64 {
f64::sin((t * PI) / 2.0)
}
}
pub struct EaseInOutSine;
impl Ease for EaseInOutSine {
fn evaluate(t: f64) -> f64 {
-(f64::cos(PI * t) - 1.0) / 2.0
}
}
pub struct EaseInQuad;
impl Ease for EaseInQuad {
fn evaluate(t: f64) -> f64 {
t * t
}
}
pub struct EaseOutQuad;
impl Ease for EaseOutQuad {
fn evaluate(t: f64) -> f64 {
(1.0 - t).mul_add(-1.0 - t, 1.0)
}
}
pub struct EaseInOutQuad;
impl Ease for EaseInOutQuad {
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0
}
}
}
pub struct EaseInCubic;
impl Ease for EaseInCubic {
fn evaluate(t: f64) -> f64 {
t * t * t
}
}
pub struct EaseOutCubic;
impl Ease for EaseOutCubic {
fn evaluate(t: f64) -> f64 {
1.0 - (1.0 - t).powi(3)
}
}
pub struct EaseInOutCubic;
impl Ease for EaseInOutCubic {
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0
}
}
}
pub struct EaseInQuart;
impl Ease for EaseInQuart {
fn evaluate(t: f64) -> f64 {
t * t * t * t
}
}
pub struct EaseOutQuart;
impl Ease for EaseOutQuart {
fn evaluate(t: f64) -> f64 {
1.0 - (1.0 - t).powi(4)
}
}
pub struct EaseInOutQuart;
impl Ease for EaseInOutQuart {
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
8.0 * t * t * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(4) / 2.0
}
}
}
pub struct EaseInQuint;
impl Ease for EaseInQuint {
fn evaluate(t: f64) -> f64 {
t * t * t * t * t
}
}
pub struct EaseOutQuint;
impl Ease for EaseOutQuint {
fn evaluate(t: f64) -> f64 {
1.0 - (1.0 - t).powi(5)
}
}
pub struct EaseInOutQuint;
impl Ease for EaseInOutQuint {
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
16.0 * t * t * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(5) / 2.0
}
}
}
pub struct EaseInExpo;
impl Ease for EaseInExpo {
fn evaluate(t: f64) -> f64 {
if t == 0.0 {
return t;
}
10.0f64.mul_add(t, -10.0).exp2()
}
}
pub struct EaseOutExpo;
impl Ease for EaseOutExpo {
fn evaluate(t: f64) -> f64 {
if (t - 1.0).abs() < f64::EPSILON {
return t;
}
1.0 - (-10.0 * t).exp2()
}
}
pub struct EaseInOutExpo;
impl Ease for EaseInOutExpo {
fn evaluate(t: f64) -> f64 {
if t == 0.0 || (t - 1.0).abs() < f64::EPSILON {
return t;
}
if t < 0.5 {
20.0f64.mul_add(t, -10.0).exp2() / 2.0
} else {
(2.0 - (-20.0f64).mul_add(t, 10.0).exp2()) / 2.0
}
}
}
pub struct EaseInCirc;
impl Ease for EaseInCirc {
fn evaluate(t: f64) -> f64 {
1.0 - f64::sqrt(t.mul_add(-t, 1.0))
}
}
pub struct EaseOutCirc;
impl Ease for EaseOutCirc {
fn evaluate(t: f64) -> f64 {
f64::sqrt((t - 1.0).mul_add(-(t - 1.0), 1.0))
}
}
pub struct EaseInOutCirc;
impl Ease for EaseInOutCirc {
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
(1.0 - f64::sqrt((2.0 * t).mul_add(-(2.0 * t), 1.0))) / 2.0
} else {
(f64::sqrt(
(-2.0f64)
.mul_add(t, 2.0)
.mul_add(-(-2.0f64).mul_add(t, 2.0), 1.0),
) + 1.0)
/ 2.0
}
}
}
pub struct EaseInBack;
impl Ease for EaseInBack {
fn evaluate(t: f64) -> f64 {
let c1 = 1.70158;
let c3 = c1 + 1.0;
(c3 * t * t).mul_add(t, -c1 * t * t)
}
}
pub struct EaseOutBack;
impl Ease for EaseOutBack {
fn evaluate(t: f64) -> f64 {
let c1: f64 = 1.70158;
let c3: f64 = c1 + 1.0;
c1.mul_add((t - 1.0).powi(2), c3.mul_add((t - 1.0).powi(3), 1.0))
}
}
pub struct EaseInOutBack;
impl Ease for EaseInOutBack {
fn evaluate(t: f64) -> f64 {
let c1: f64 = 1.70158;
let c2: f64 = c1 * 1.525;
if t < 0.5 {
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0).mul_add(t, -c2)) / 2.0
} else {
((2.0f64.mul_add(t, -2.0))
.powi(2)
.mul_add((c2 + 1.0).mul_add(t.mul_add(2.0, -2.0), c2), 2.0))
/ 2.0
}
}
}
pub struct EaseInElastic;
impl Ease for EaseInElastic {
fn evaluate(t: f64) -> f64 {
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
return t;
}
let c4 = (2.0 * PI) / 3.0;
-(10.0f64.mul_add(t, -10.0).exp2()) * f64::sin(t.mul_add(10.0, -10.75) * c4)
}
}
pub struct EaseOutElastic;
impl Ease for EaseOutElastic {
fn evaluate(t: f64) -> f64 {
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
return t;
}
let c4 = (2.0 * PI) / 3.0;
(-10.0 * t)
.exp2()
.mul_add(f64::sin(t.mul_add(10.0, -0.75) * c4), 1.0)
}
}
pub struct EaseInOutElastic;
impl Ease for EaseInOutElastic {
fn evaluate(t: f64) -> f64 {
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
return t;
}
let c5 = (2.0 * PI) / 4.5;
if t < 0.5 {
-(20.0f64.mul_add(t, -10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
} else {
((-20.0f64).mul_add(t, 10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
+ 1.0
}
}
}
pub struct EaseInBounce;
impl Ease for EaseInBounce {
fn evaluate(t: f64) -> f64 {
1.0 - EaseOutBounce::evaluate(1.0 - t)
}
}
pub struct EaseOutBounce;
impl Ease for EaseOutBounce {
fn evaluate(t: f64) -> f64 {
let mut time = t;
let n1 = 7.5625;
let d1 = 2.75;
if t < 1.0 / d1 {
n1 * time * time
} else if time < 2.0 / d1 {
time -= 1.5 / d1;
(n1 * time).mul_add(time, 0.75)
} else if time < 2.5 / d1 {
time -= 2.25 / d1;
(n1 * time).mul_add(time, 0.9375)
} else {
time -= 2.625 / d1;
(n1 * time).mul_add(time, 0.984_375)
}
}
}
pub struct EaseInOutBounce;
impl Ease for EaseInOutBounce {
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
(1.0 - EaseOutBounce::evaluate(2.0f64.mul_add(-t, 1.0))) / 2.0
} else {
(1.0 + EaseOutBounce::evaluate(2.0f64.mul_add(t, -1.0))) / 2.0
}
}
}
fn apply_ease_func(t: f64) -> f64 {
let style = *ANIMATION_STYLE.lock();
match style {
AnimationStyle::Linear => Linear::evaluate(t),
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
AnimationStyle::EaseOutSine => EaseOutSine::evaluate(t),
AnimationStyle::EaseInOutSine => EaseInOutSine::evaluate(t),
AnimationStyle::EaseInQuad => EaseInQuad::evaluate(t),
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
AnimationStyle::EaseInOutQuart => EaseInOutQuart::evaluate(t),
AnimationStyle::EaseInQuint => EaseInQuint::evaluate(t),
AnimationStyle::EaseOutQuint => EaseOutQuint::evaluate(t),
AnimationStyle::EaseInOutQuint => EaseInOutQuint::evaluate(t),
AnimationStyle::EaseInExpo => EaseInExpo::evaluate(t),
AnimationStyle::EaseOutExpo => EaseOutExpo::evaluate(t),
AnimationStyle::EaseInOutExpo => EaseInOutExpo::evaluate(t),
AnimationStyle::EaseInCirc => EaseInCirc::evaluate(t),
AnimationStyle::EaseOutCirc => EaseOutCirc::evaluate(t),
AnimationStyle::EaseInOutCirc => EaseInOutCirc::evaluate(t),
AnimationStyle::EaseInBack => EaseInBack::evaluate(t),
AnimationStyle::EaseOutBack => EaseOutBack::evaluate(t),
AnimationStyle::EaseInOutBack => EaseInOutBack::evaluate(t),
AnimationStyle::EaseInElastic => EaseInElastic::evaluate(t),
AnimationStyle::EaseOutElastic => EaseOutElastic::evaluate(t),
AnimationStyle::EaseInOutElastic => EaseInOutElastic::evaluate(t),
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
}
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct Animation {
pub hwnd: isize,
}
impl Animation {
pub fn new(hwnd: isize) -> Self {
Self { hwnd }
}
pub fn cancel(&mut self) {
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
return;
}
ANIMATION_MANAGER.lock().cancel(self.hwnd);
let max_duration = Duration::from_secs(1);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(self.hwnd);
}
std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
));
}
}
#[allow(clippy::cast_possible_truncation)]
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
let time = apply_ease_func(t);
f64::from(end - start)
.mul_add(time, f64::from(start))
.round() as i32
}
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
Rect {
left: Self::lerp(start_rect.left, end_rect.left, t),
top: Self::lerp(start_rect.top, end_rect.top, t),
right: Self::lerp(start_rect.right, end_rect.right, t),
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
}
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
&mut self,
duration: Duration,
mut render_callback: impl FnMut(f64) -> Result<()>,
) -> Result<()> {
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
self.cancel();
}
ANIMATION_MANAGER.lock().start(self.hwnd);
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
// cancel animation
// set all flags
ANIMATION_MANAGER.lock().end(self.hwnd);
return Ok(());
}
let frame_start = Instant::now();
// calculate progress
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_callback(progress).ok();
// sleep until next frame
if frame_start.elapsed() < target_frame_time {
std::thread::sleep(target_frame_time - frame_start.elapsed());
}
}
ANIMATION_MANAGER.lock().end(self.hwnd);
// limit progress to 1.0 if animation took longer
if progress > 1.0 {
progress = 1.0;
}
// process animation for 1.0 to set target position
render_callback(progress)
}
}

View File

@@ -1,79 +0,0 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub is_cancelled: bool,
}
#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<isize, AnimationState>,
}
impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}
impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}
pub fn is_cancelled(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.is_cancelled
} else {
false
}
}
pub fn in_progress(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.in_progress
} else {
false
}
}
pub fn cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.is_cancelled = true;
}
}
pub fn start(&mut self, hwnd: isize) {
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
e.insert(AnimationState {
in_progress: true,
is_cancelled: false,
});
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
return;
}
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = true;
}
}
pub fn end(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
animation_state.is_cancelled = false;
self.animations.remove(&hwnd);
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
}
}
}

108
komorebi/src/border.rs Normal file
View File

@@ -0,0 +1,108 @@
use std::sync::atomic::Ordering;
use color_eyre::Result;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::FindWindowW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use crate::window::Window;
use crate::windows_callbacks;
use crate::WindowsApi;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_RECT;
use crate::BORDER_WIDTH;
use crate::TRANSPARENCY_COLOUR;
#[derive(Debug, Clone, Copy)]
pub struct Border {
pub(crate) hwnd: isize,
}
impl From<isize> for Border {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Border {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
pub fn create(name: &str) -> Result<()> {
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
let instance = WindowsApi::module_handle_w()?;
let class_name = PCWSTR(name.as_ptr());
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
let window_class = WNDCLASSW {
hInstance: instance.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(windows_callbacks::border_window),
hbrBackground: brush,
..Default::default()
};
let _atom = WindowsApi::register_class_w(&window_class)?;
let name_cl = name.clone();
std::thread::spawn(move || -> Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name_cl.as_ptr()), instance)?;
let border = Self::from(hwnd);
let mut message = MSG::default();
unsafe {
while GetMessageW(&mut message, border.hwnd(), 0, 0).into() {
DispatchMessageW(&message);
}
}
Ok(())
});
let mut hwnd = HWND(0);
while hwnd == HWND(0) {
hwnd = unsafe { FindWindowW(PCWSTR(name.as_ptr()), PCWSTR::null()) };
}
BORDER_HWND.store(hwnd.0, Ordering::SeqCst);
Ok(())
}
pub fn hide(self) -> Result<()> {
if self.hwnd == 0 {
Ok(())
} else {
WindowsApi::hide_border_window(self.hwnd())
}
}
pub fn set_position(self, window: Window, activate: bool) -> Result<()> {
if self.hwnd == 0 {
Ok(())
} else {
if !WindowsApi::is_window(self.hwnd()) {
Self::create("komorebi-border-window")?;
}
let mut rect = WindowsApi::window_rect(window.hwnd())?;
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
rect.add_margin(border_width);
*BORDER_RECT.lock() = rect;
WindowsApi::position_border_window(self.hwnd(), &rect, activate)
}
}
}

View File

@@ -1,225 +0,0 @@
use crate::border_manager::window_kind_colour;
use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUS_STATE;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::WindowsApi;
use crate::WINDOWS_11;
use komorebi_core::BorderStyle;
use komorebi_core::Rect;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
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;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
if class.starts_with("komoborder") {
hwnds.push(hwnd.0);
}
}
true.into()
}
#[derive(Debug)]
pub struct Border {
pub hwnd: isize,
}
impl From<isize> for Border {
fn from(value: isize) -> Self {
Self { hwnd: value }
}
}
impl Border {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let window_class = WNDCLASSW {
hInstance: h_module.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), h_module)?;
hwnd_sender.send(hwnd)?;
let mut msg: MSG = MSG::default();
loop {
unsafe {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
tracing::debug!("border window event processing thread shutdown");
break;
};
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
std::thread::sleep(Duration::from_millis(10))
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?,
})
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::close_window(self.hwnd())
}
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
// Make adjustments to the border
let mut rect = *rect;
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
// Update the position of the border if required
if !WindowsApi::window_rect(self.hwnd())?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd(), &rect, HWND((Z_ORDER.load()).into()))?;
should_invalidate = true;
}
// Invalidate the rect to trigger the callback to update colours etc.
if should_invalidate {
self.invalidate();
}
Ok(())
}
pub fn invalidate(&self) {
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
}
pub extern "system" fn callback(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_PAINT => {
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
// With the rect that we set in Self::update
match WindowsApi::window_rect(window) {
Ok(rect) => {
// Grab the focus kind for this border
let window_kind = {
FOCUS_STATE
.lock()
.get(&window.0)
.copied()
.unwrap_or(WindowKind::Unfocused)
};
// Set up the brush to draw the border
let hpen = CreatePen(
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(window_kind_colour(window_kind)),
);
let hbrush = WindowsApi::create_solid_brush(0);
// Draw the border
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
// TODO(raggi): this is approximately the correct curvature for
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
// often the bottom right has a different shape. Furthermore if
// the window was made with DWMWCP_ROUNDSMALL then this is the
// wrong size. In the future we should read the DWM properties
// of windows and attempt to match appropriately.
match STYLE.load() {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
BorderStyle::Rounded => {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
}
BorderStyle::Square => {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
DeleteObject(hpen);
DeleteObject(hbrush);
}
Err(error) => {
tracing::error!("could not get border rect: {}", error.to_string())
}
}
EndPaint(window, &ps);
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
}

View File

@@ -1,481 +0,0 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::BorderImplementation;
use komorebi_core::BorderStyle;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
use crate::ring::Ring;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::Colour;
use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
use border::Border;
use komorebi_core::WindowKind;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
lazy_static! {
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
AtomicCell::new(BorderImplementation::Windows);
pub static ref FOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
pub static ref UNFOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(128, 128, 128))));
pub static ref MONOCLE: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
}
lazy_static! {
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
}
pub struct Notification;
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification() {
if event_tx().try_send(Notification).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn destroy_all_borders() -> color_eyre::Result<()> {
let mut borders = BORDER_STATE.lock();
tracing::info!(
"purging known borders: {:?}",
borders.iter().map(|b| b.1.hwnd).collect::<Vec<_>>()
);
for (_, border) in borders.iter() {
border.destroy()?;
}
borders.clear();
BORDERS_MONITORS.lock().clear();
FOCUS_STATE.lock().clear();
let mut remaining_hwnds = vec![];
WindowsApi::enum_windows(
Some(border_hwnds),
&mut remaining_hwnds as *mut Vec<isize> as isize,
)?;
if !remaining_hwnds.is_empty() {
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
for hwnd in remaining_hwnds {
Border::from(hwnd).destroy()?;
}
}
Ok(())
}
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
match focus_kind {
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
let receiver = event_rx();
event_tx().send(Notification)?;
let mut previous_snapshot = Ring::default();
let mut previous_pending_move_op = None;
let mut previous_is_paused = false;
'receiver: for _ in receiver {
// Check the wm state every time we receive a notification
let state = wm.lock();
let is_paused = state.is_paused;
let focused_monitor_idx = state.focused_monitor_idx();
let monitors = state.monitors.clone();
let pending_move_op = state.pending_move_op;
drop(state);
match IMPLEMENTATION.load() {
BorderImplementation::Windows => {
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let window_kind = if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
};
monocle
.focused_window()
.copied()
.unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?;
continue 'monitors;
}
for (idx, c) in ws.containers().iter().enumerate() {
let window_kind = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
WindowKind::Stack
} else {
WindowKind::Single
};
c.focused_window()
.copied()
.unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?;
}
}
}
}
BorderImplementation::Komorebi => {
let mut should_process_notification = true;
if monitors == previous_snapshot
// handle the window dragging edge case
&& pending_move_op == previous_pending_move_op
{
should_process_notification = false;
}
// handle the pause edge case
if is_paused && !previous_is_paused {
should_process_notification = true;
}
// handle the unpause edge case
if previous_is_paused && !is_paused {
should_process_notification = true;
}
// handle the retile edge case
if !should_process_notification && BORDER_STATE.lock().is_empty() {
should_process_notification = true;
}
if !should_process_notification {
tracing::trace!("monitor state matches latest snapshot, skipping notification");
continue 'receiver;
}
let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();
// If borders are disabled
if !BORDER_ENABLED.load_consume()
// Or if they are temporarily disabled
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
// Or if the wm is paused
|| is_paused
// Or if we are handling an alt-tab across workspaces
|| ALT_TAB_HWND.load().is_some()
{
// Destroy the borders we know about
for (_, border) in borders.iter() {
border.destroy()?;
}
borders.clear();
previous_is_paused = is_paused;
continue 'receiver;
}
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Workspaces with tiling disabled don't have borders
if !ws.tile() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default()
== monitor_idx
{
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'monitors;
}
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let border = match borders.entry(monocle.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(monocle.id()) {
entry.insert(border)
} else {
continue 'monitors;
}
}
};
borders_monitors.insert(monocle.id().clone(), monitor_idx);
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(
border.hwnd,
if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
},
);
}
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd(),
)?;
border.update(&rect, true)?;
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
for (id, b) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default()
== monitor_idx
&& border_hwnd != b.hwnd
{
b.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'monitors;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
if is_maximized {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default()
== monitor_idx
{
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'monitors;
}
// Destroy any borders 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, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
{
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
for (idx, c) in ws.containers().iter().enumerate() {
// Update border when moving or resizing with mouse
if pending_move_op.is_some() && idx == ws.focused_container_idx() {
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
let mut rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
while WindowsApi::lbutton_is_pressed() {
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'monitors;
}
}
};
let new_rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect, true)?;
}
}
Z_ORDER.store(restore_z_order);
continue 'monitors;
}
// Get the border entry for this container from the map or create one
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'monitors;
}
}
};
borders_monitors.insert(c.id().clone(), monitor_idx);
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
WindowKind::Stack
} else {
WindowKind::Single
};
// Update the focused state for all containers on this workspace
{
let mut focus_state = FOCUS_STATE.lock();
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
}
let rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => last_focus_state != new_focus_state,
};
border.update(&rect, should_invalidate)?;
}
}
}
}
}
previous_snapshot = monitors;
previous_pending_move_op = pending_move_op;
previous_is_paused = is_paused;
}
Ok(())
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub enum ZOrder {
Top,
NoTopMost,
Bottom,
TopMost,
}
impl From<ZOrder> for isize {
fn from(val: ZOrder) -> Self {
match val {
ZOrder::Top => 0,
ZOrder::NoTopMost => -2,
ZOrder::Bottom => 1,
ZOrder::TopMost => -1,
}
}
}

View File

@@ -65,7 +65,7 @@ pub struct Rgb {
}
impl Rgb {
pub const fn new(r: u32, g: u32, b: u32) -> Self {
pub fn new(r: u32, g: u32, b: u32) -> Self {
Self { r, g, b }
}
}

View File

@@ -7,13 +7,18 @@ use serde::Deserialize;
use serde::Serialize;
use crate::ring::Ring;
use crate::stackbar::Stackbar;
use crate::window::Window;
use crate::StackbarMode;
use crate::STACKBAR_MODE;
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
#[getset(get = "pub")]
id: String,
windows: Ring<Window>,
#[getset(get = "pub", get_mut = "pub")]
stackbar: Option<Stackbar>,
}
impl_ring_elements!(Container, Window);
@@ -23,6 +28,10 @@ impl Default for Container {
Self {
id: nanoid!(),
windows: Ring::default(),
stackbar: match *STACKBAR_MODE.lock() {
StackbarMode::Always => Stackbar::create().ok(),
StackbarMode::Never | StackbarMode::OnStack => None,
},
}
}
}
@@ -35,6 +44,10 @@ impl PartialEq for Container {
impl Container {
pub fn hide(&self, omit: Option<isize>) {
if let Some(stackbar) = self.stackbar() {
stackbar.hide();
}
for window in self.windows().iter().rev() {
let mut should_hide = omit.is_none();
@@ -53,6 +66,10 @@ impl Container {
}
pub fn restore(&self) {
if let Some(stackbar) = self.stackbar() {
stackbar.restore();
}
if let Some(window) = self.focused_window() {
window.restore();
}
@@ -104,7 +121,15 @@ impl Container {
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
let window = self.windows_mut().remove(idx);
self.focus_window(idx.saturating_sub(1));
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack) && self.windows().len() <= 1 {
self.stackbar = None;
}
if idx != 0 {
self.focus_window(idx - 1);
};
window
}
@@ -115,14 +140,15 @@ impl Container {
pub fn add_window(&mut self, window: Window) {
self.windows_mut().push_back(window);
self.focus_window(self.windows().len().saturating_sub(1));
let focused_window_idx = self.focused_window_idx();
for (i, window) in self.windows().iter().enumerate() {
if i != focused_window_idx {
window.hide();
}
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack)
&& self.windows().len() > 1
&& self.stackbar.is_none()
{
self.stackbar = Stackbar::create().ok();
}
self.focus_window(self.windows().len() - 1);
}
#[tracing::instrument(skip(self))]

76
komorebi/src/hidden.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::sync::atomic::Ordering;
use color_eyre::Result;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::FindWindowW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use crate::windows_callbacks;
use crate::WindowsApi;
use crate::HIDDEN_HWND;
use crate::TRANSPARENCY_COLOUR;
#[derive(Debug, Clone, Copy)]
pub struct Hidden {
pub(crate) hwnd: isize,
}
impl From<isize> for Hidden {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Hidden {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
pub fn create(name: &str) -> Result<()> {
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
let instance = WindowsApi::module_handle_w()?;
let class_name = PCWSTR(name.as_ptr());
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
let window_class = WNDCLASSW {
hInstance: instance.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(windows_callbacks::hidden_window),
hbrBackground: brush,
..Default::default()
};
let _atom = WindowsApi::register_class_w(&window_class)?;
let name_cl = name.clone();
std::thread::spawn(move || -> Result<()> {
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name_cl.as_ptr()), instance)?;
let hidden = Self::from(hwnd);
let mut message = MSG::default();
unsafe {
while GetMessageW(&mut message, hidden.hwnd(), 0, 0).into() {
DispatchMessageW(&message);
}
}
Ok(())
});
let mut hwnd = HWND(0);
while hwnd == HWND(0) {
hwnd = unsafe { FindWindowW(PCWSTR(name.as_ptr()), PCWSTR::null()) };
}
HIDDEN_HWND.store(hwnd.0, Ordering::SeqCst);
Ok(())
}
}

View File

@@ -1,24 +1,18 @@
#![warn(clippy::all)]
pub mod animation;
pub mod animation_manager;
pub mod border_manager;
pub mod border;
pub mod com;
#[macro_use]
pub mod ring;
pub mod colour;
pub mod container;
pub mod hidden;
pub mod monitor;
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 stackbar;
pub mod static_config;
pub mod styles;
pub mod transparency_manager;
pub mod window;
pub mod window_manager;
pub mod window_manager_event;
@@ -27,7 +21,6 @@ pub mod windows_callbacks;
pub mod winevent;
pub mod winevent_listener;
pub mod workspace;
pub mod workspace_reconciliator;
use lazy_static::lazy_static;
use std::collections::HashMap;
@@ -39,18 +32,16 @@ use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use animation::*;
pub use animation_manager::*;
pub use colour::*;
pub use hidden::*;
pub use process_command::*;
pub use process_event::*;
pub use stackbar::*;
pub use static_config::*;
pub use window::*;
pub use window_manager::*;
pub use window_manager_event::*;
pub use windows_api::WindowsApi;
@@ -60,7 +51,6 @@ use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::AnimationStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
@@ -203,16 +193,18 @@ lazy_static! {
)
};
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
Arc::new(Mutex::new(AnimationStyle::Linear));
static ref ACTIVE_WINDOW_BORDER_STYLE: Arc<Mutex<ActiveWindowBorderStyle>> =
Arc::new(Mutex::new(ActiveWindowBorderStyle::System));
static ref BORDER_RECT: Arc<Mutex<Rect>> =
Arc::new(Mutex::new(Rect::default()));
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
// Use app-specific titlebar removal options where possible
// eg. Windows Terminal, IntelliJ IDEA, Firefox
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref STACKBAR_MODE: Arc<Mutex<StackbarMode >> = Arc::new(Mutex::new(StackbarMode::Never));
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -224,10 +216,27 @@ pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false);
pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0);
pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false);
pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
pub const TRANSPARENCY_COLOUR: u32 = 0;
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {

View File

@@ -1,10 +1,9 @@
#![warn(clippy::all)]
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(
clippy::missing_errors_doc,
clippy::redundant_pub_crate,
clippy::significant_drop_tightening,
clippy::significant_drop_in_scrutinee,
clippy::doc_markdown
clippy::significant_drop_in_scrutinee
)]
use std::path::PathBuf;
@@ -24,29 +23,22 @@ use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use komorebi::border_manager;
use komorebi::hidden::Hidden;
use komorebi::load_configuration;
use komorebi::monitor_reconciliator;
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::transparency_manager;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
use komorebi::winevent_listener;
use komorebi::workspace_reconciliator;
use komorebi::CUSTOM_FFM;
use komorebi::DATA_DIR;
use komorebi::HOME_DIR;
use komorebi::INITIAL_CONFIGURATION_LOADED;
use komorebi::SESSION_ID;
shadow_rs::shadow!(build);
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
@@ -58,8 +50,8 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
std::env::set_var("RUST_LOG", "info");
}
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
let color_appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi.log");
let appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi_plaintext.log");
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
@@ -134,7 +126,7 @@ fn detect_deadlocks() {
}
#[derive(Parser)]
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
#[clap(author, about, version)]
struct Opts {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(short, long = "ffm")]
@@ -194,6 +186,8 @@ fn main() -> Result<()> {
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
Hidden::create("komorebi-hidden")?;
let static_config = opts.config.map_or_else(
|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
@@ -259,13 +253,6 @@ fn main() -> Result<()> {
listen_for_movements(wm.clone());
}
border_manager::listen_for_notifications(wm.clone());
stackbar_manager::listen_for_notifications(wm.clone());
transparency_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 || {
ctrlc_sender

View File

@@ -19,16 +19,7 @@ use crate::ring::Ring;
use crate::workspace::Workspace;
#[derive(
Debug,
Clone,
Serialize,
Deserialize,
Getters,
CopyGetters,
MutGetters,
Setters,
JsonSchema,
PartialEq,
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
@@ -36,19 +27,15 @@ pub struct Monitor {
#[getset(get = "pub", set = "pub")]
name: String,
#[getset(get = "pub", set = "pub")]
device: String,
device: Option<String>,
#[getset(get = "pub", set = "pub")]
device_id: String,
device_id: Option<String>,
#[getset(get = "pub", set = "pub")]
size: Rect,
#[getset(get = "pub", set = "pub")]
work_area_size: Rect,
#[getset(get_copy = "pub", set = "pub")]
work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
window_based_work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
window_based_work_area_offset_limit: isize,
workspaces: Ring<Workspace>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
@@ -59,27 +46,18 @@ pub struct Monitor {
impl_ring_elements!(Monitor, Workspace);
pub fn new(
id: isize,
size: Rect,
work_area_size: Rect,
name: String,
device: String,
device_id: String,
) -> Monitor {
pub fn new(id: isize, size: Rect, work_area_size: Rect, name: String) -> Monitor {
let mut workspaces = Ring::default();
workspaces.elements_mut().push_back(Workspace::default());
Monitor {
id,
name,
device,
device_id,
device: None,
device_id: None,
size,
work_area_size,
work_area_offset: None,
window_based_work_area_offset: None,
window_based_work_area_offset_limit: 1,
workspaces,
last_focused_workspace: None,
workspace_names: HashMap::default(),
@@ -127,7 +105,7 @@ impl Monitor {
if idx == 0 {
self.workspaces_mut().push_back(Workspace::default());
} else {
self.focus_workspace(idx.saturating_sub(1)).ok()?;
self.focus_workspace(idx - 1).ok()?;
};
None
@@ -216,11 +194,6 @@ impl Monitor {
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
let work_area = *self.work_area_size();
let window_based_work_area_offset = (
self.window_based_work_area_offset_limit(),
self.window_based_work_area_offset(),
);
let offset = if self.work_area_offset().is_some() {
self.work_area_offset()
} else {
@@ -229,7 +202,7 @@ impl Monitor {
self.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?
.update(&work_area, offset, window_based_work_area_offset)?;
.update(&work_area, offset)?;
Ok(())
}

View File

@@ -1,203 +0,0 @@
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
use crate::monitor_reconciliator;
use crate::WindowsApi;
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
#[derive(Debug, Clone, Copy)]
pub struct Hidden {
pub hwnd: isize,
}
impl From<isize> for Hidden {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Hidden {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
pub fn create(name: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let window_class = WNDCLASSW {
hInstance: h_module.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
let _ = WindowsApi::register_class_w(&window_class)?;
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name.as_ptr()), h_module)?;
hwnd_sender.send(hwnd)?;
let mut msg: MSG = MSG::default();
loop {
unsafe {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
tracing::debug!("hidden window event processing thread shutdown");
break;
};
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
std::thread::sleep(Duration::from_millis(10))
}
Ok(())
});
let hwnd = hwnd_receiver.recv()?;
WindowsApi::wts_register_session_notification(hwnd)?;
Ok(Self { hwnd })
}
pub extern "system" fn callback(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_POWERBROADCAST => {
match wparam.0 as u32 {
// Automatic: System resumed itself from sleep or hibernation
// Suspend: User resumed system from sleep or hibernation
PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => {
tracing::debug!(
"WM_POWERBROADCAST event received - resume from suspend"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::ResumingFromSuspendedState,
);
LRESULT(0)
}
// Computer is entering a suspended state
PBT_APMSUSPEND => {
tracing::debug!(
"WM_POWERBROADCAST event received - entering suspended state"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::EnteringSuspendedState,
);
LRESULT(0)
}
_ => LRESULT(0),
}
}
WM_WTSSESSION_CHANGE => {
match wparam.0 as u32 {
WTS_SESSION_LOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::SessionLocked,
);
}
WTS_SESSION_UNLOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::SessionUnlocked,
);
}
_ => {}
}
LRESULT(0)
}
// This event gets sent when:
// - The scaling factor on a display changes
// - The resolution on a display changes
// - A monitor is added
// - A monitor is removed
// Since WM_DEVICECHANGE also notifies on monitor changes, we only handle scaling
// and resolution changes here
WM_DISPLAYCHANGE => {
tracing::debug!(
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::ResolutionScalingChanged,
);
LRESULT(0)
}
// Unfortunately this is the event sent with ButteryTaskbar which I use a lot
// Original idea from https://stackoverflow.com/a/33762334
WM_SETTINGCHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
tracing::debug!(
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::WorkAreaChanged,
);
}
LRESULT(0)
}
// This event + wparam combo is sent 4 times when a monitor is added based on my testing on win11
// Original idea from https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
tracing::debug!(
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::DisplayConnectionChange,
);
}
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
}

View File

@@ -1,410 +0,0 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
use crate::MonitorConfig;
use crate::WindowManager;
use crate::WindowsApi;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::Rect;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
pub mod hidden;
pub enum Notification {
ResolutionScalingChanged,
WorkAreaChanged,
DisplayConnectionChange,
EnteringSuspendedState,
ResumingFromSuspendedState,
SessionLocked,
SessionUnlocked,
}
static ACTIVE: AtomicBool = AtomicBool::new(true);
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification(notification: Notification) {
if event_tx().try_send(notification).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
monitor_cache.insert(device_id.to_string(), config);
}
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
Ok(win32_display_data::connected_displays_all()
.flatten()
.map(|display| {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
monitor::new(
display.hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
)
})
.collect::<Vec<_>>())
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
#[allow(clippy::expect_used)]
Hidden::create("komorebi-hidden")?;
tracing::info!("created hidden window to listen for monitor-related events");
std::thread::spawn(move || loop {
match handle_notifications(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)
}
}
}
});
Ok(())
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
'receiver: for notification in receiver {
if !ACTIVE.load_consume() {
if matches!(
notification,
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked
) {
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
);
ACTIVE.store(true, Ordering::SeqCst);
}
continue 'receiver;
}
let mut wm = wm.lock();
match notification {
Notification::EnteringSuspendedState | Notification::SessionLocked => {
tracing::debug!(
"deactivating reconciliator until system resumes from suspended state or session is unlocked"
);
ACTIVE.store(false, Ordering::SeqCst);
}
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => {
// this is only handled above if the reconciliator is paused
}
Notification::WorkAreaChanged => {
tracing::debug!("handling work area changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
let mut should_update = false;
// Update work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
}
if should_update {
tracing::info!("updated work area for {}", monitor.device_id());
monitor.update_focused_workspace(offset)?;
border_manager::send_notification();
} else {
tracing::debug!(
"work areas match, reconciliation not required for {}",
monitor.device_id()
);
}
}
}
Notification::ResolutionScalingChanged => {
tracing::debug!("handling resolution/scaling changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
let mut should_update = false;
// Update sizes and work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
if reference.size() != monitor.size() {
monitor.set_size(Rect {
left: reference.size().left,
top: reference.size().top,
right: reference.size().right,
bottom: reference.size().bottom,
});
should_update = true;
}
}
if should_update {
tracing::info!(
"updated monitor resolution/scaling for {}",
monitor.device_id()
);
monitor.update_focused_workspace(offset)?;
border_manager::send_notification();
} else {
tracing::debug!(
"resolutions match, reconciliation not required for {}",
monitor.device_id()
);
}
}
}
Notification::DisplayConnectionChange => {
tracing::debug!("handling display connection change notification");
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
let initial_monitor_count = wm.monitors().len();
// Get the currently attached display devices
let attached_devices = attached_display_devices()?;
// Make sure that in our state any attached displays have the latest Win32 data
for monitor in wm.monitors_mut() {
for attached in &attached_devices {
if attached.device_id().eq(monitor.device_id()) {
monitor.set_id(attached.id());
monitor.set_name(attached.name().clone());
monitor.set_size(*attached.size());
monitor.set_work_area_size(*attached.work_area_size());
}
}
}
if initial_monitor_count == attached_devices.len() {
tracing::debug!("monitor counts match, reconciliation not required");
continue 'receiver;
}
if attached_devices.is_empty() {
tracing::debug!(
"no devices found, skipping reconciliation to avoid breaking state"
);
continue 'receiver;
}
if initial_monitor_count > attached_devices.len() {
tracing::info!(
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
attached_devices.len()
);
// Gather all the containers that will be orphaned from disconnected and invalid displays
let mut orphaned_containers = vec![];
// Collect the ids in our state which aren't in the current attached display ids
// These are monitors that have been removed
let mut newly_removed_displays = vec![];
for m in wm.monitors().iter() {
if !attached_devices
.iter()
.any(|attached| attached.device_id().eq(m.device_id()))
{
newly_removed_displays.push(m.device_id().clone());
for workspace in m.workspaces() {
for container in workspace.containers() {
// Save the orphaned containers from the removed monitor
orphaned_containers.push(container.clone());
}
}
// Let's add their state to the cache for later
monitor_cache.insert(m.device_id().clone(), m.into());
}
}
if !orphaned_containers.is_empty() {
tracing::info!(
"removed orphaned containers from: {newly_removed_displays:?}"
);
}
if !newly_removed_displays.is_empty() {
// After we have cached them, remove them from our state
wm.monitors_mut()
.retain(|m| !newly_removed_displays.contains(m.device_id()));
}
let post_removal_monitor_count = wm.monitors().len();
let focused_monitor_idx = wm.focused_monitor_idx();
if focused_monitor_idx >= post_removal_monitor_count {
wm.focus_monitor(0)?;
}
if !orphaned_containers.is_empty() {
if let Some(primary) = wm.monitors_mut().front_mut() {
if let Some(focused_ws) = primary.focused_workspace_mut() {
let focused_container_idx = focused_ws.focused_container_idx();
// Put the orphaned containers somewhere visible
for container in orphaned_containers {
focused_ws.add_container(container);
}
// Gotta reset the focus or the movement will feel "off"
if initial_monitor_count != post_removal_monitor_count {
focused_ws.focus_container(focused_container_idx);
}
}
}
}
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
// If we have lost a monitor, update everything to filter out any jank
if initial_monitor_count != post_removal_monitor_count {
monitor.update_focused_workspace(offset)?;
}
}
}
let post_removal_monitor_count = wm.monitors().len();
// This is the list of device ids after we have removed detached displays
let post_removal_device_ids = wm
.monitors()
.iter()
.map(Monitor::device_id)
.cloned()
.collect::<Vec<_>>();
// Check for and add any new monitors that may have been plugged in
// Monitor and display index preferences get applied in this function
WindowsApi::load_monitor_information(&mut wm.monitors)?;
let post_addition_monitor_count = wm.monitors().len();
if post_addition_monitor_count > post_removal_monitor_count {
tracing::info!(
"monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors",
);
// Look in the updated state for new monitors
for m in wm.monitors_mut() {
let device_id = m.device_id().clone();
// We identify a new monitor when we encounter a new device id
if !post_removal_device_ids.contains(&device_id) {
let mut cache_hit = false;
// Check if that device id exists in the cache for this session
if let Some(cached) = monitor_cache.get(&device_id) {
cache_hit = true;
tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying");
// If it does, load all the monitor settings from the cache entry
m.ensure_workspace_count(cached.workspaces.len());
m.set_work_area_offset(cached.work_area_offset);
m.set_window_based_work_area_offset(
cached.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
cached.window_based_work_area_offset_limit.unwrap_or(1),
);
for (w_idx, workspace) in m.workspaces_mut().iter_mut().enumerate()
{
if let Some(cached_workspace) = cached.workspaces.get(w_idx) {
workspace.load_static_config(cached_workspace)?;
}
}
}
// Entries in the cache should only be used once; remove the entry there was a cache hit
if cache_hit {
monitor_cache.remove(&device_id);
}
}
}
}
let final_count = wm.monitors().len();
if post_removal_monitor_count != final_count {
wm.retile_all(true)?;
// Second retile to fix DPI/resolution related jank
wm.retile_all(true)?;
// Border updates to fix DPI/resolution related jank
border_manager::send_notification();
}
}
}
}
Ok(())
}

View File

@@ -27,7 +27,6 @@ use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
use komorebi_core::BorderImplementation;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::Layout;
use komorebi_core::MoveBehaviour;
@@ -39,29 +38,26 @@ use komorebi_core::StateQuery;
use komorebi_core::WindowContainerBehaviour;
use komorebi_core::WindowKind;
use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::border::Border;
use crate::colour::Rgb;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
use crate::static_config::StaticConfig;
use crate::transparency_manager;
use crate::window::RuleDebug;
use crate::window::Window;
use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HIDDEN;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
@@ -78,45 +74,30 @@ use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::TCP_CONNECTIONS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
use crate::WORKSPACE_RULES;
use stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use stackbar_manager::STACKBAR_LABEL;
use stackbar_manager::STACKBAR_MODE;
use stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use stackbar_manager::STACKBAR_TAB_HEIGHT;
use stackbar_manager::STACKBAR_TAB_WIDTH;
use stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
#[tracing::instrument]
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
let wm = wm.clone();
let listener = wm
.lock()
.command_listener
.try_clone()
.expect("could not clone unix listener");
let _ = std::thread::spawn(move || {
let listener = wm
.lock()
.command_listener
.try_clone()
.expect("could not clone unix listener");
tracing::info!("listening on komorebi.sock");
for client in listener.incoming() {
match client {
Ok(stream) => match read_commands_uds(&wm, stream) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
},
Err(error) => {
tracing::error!("{}", error);
break;
}
std::thread::spawn(move || {
tracing::info!("listening on komorebi.sock");
for client in listener.incoming() {
match client {
Ok(stream) => match read_commands_uds(&wm, stream) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
},
Err(error) => {
tracing::error!("{}", error);
break;
}
}
})
.join();
tracing::error!("restarting failed thread");
}
});
}
@@ -184,6 +165,21 @@ impl WindowManager {
}
}
match message {
SocketMessage::CycleFocusMonitor(_)
| SocketMessage::CycleFocusWorkspace(_)
| SocketMessage::FocusMonitorNumber(_)
| SocketMessage::FocusMonitorWorkspaceNumber(_, _)
| SocketMessage::FocusWorkspaceNumber(_) => {
if self.focused_workspace()?.visible_windows().is_empty() {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
}
_ => {}
};
match message {
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
if let Some(monitor) = self.focused_monitor_mut() {
@@ -204,10 +200,6 @@ impl WindowManager {
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
SocketMessage::PromoteWindow(direction) => {
self.focus_container_in_direction(direction)?;
self.promote_container_to_front()?
}
SocketMessage::FocusWindow(direction) => {
self.focus_container_in_direction(direction)?;
}
@@ -222,8 +214,6 @@ 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)?;
@@ -234,12 +224,8 @@ impl WindowManager {
WindowsApi::center_cursor_in_rect(&focused_window_rect)?;
WindowsApi::left_click();
}
SocketMessage::Close => {
Window::from(WindowsApi::foreground_window()?).close()?;
}
SocketMessage::Minimize => {
Window::from(WindowsApi::foreground_window()?).minimize();
}
SocketMessage::Close => self.focused_window()?.close()?,
SocketMessage::Minimize => self.focused_window()?.minimize(),
SocketMessage::ToggleFloat => self.toggle_float()?,
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
@@ -283,40 +269,6 @@ impl WindowManager {
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
}
}
SocketMessage::ClearWorkspaceRules(monitor_idx, workspace_idx) => {
let mut workspace_rules = WORKSPACE_RULES.lock();
let mut to_remove = vec![];
for (id, (m_idx, w_idx, _)) in workspace_rules.iter() {
if monitor_idx == *m_idx && workspace_idx == *w_idx {
to_remove.push(id.clone());
}
}
for rule in to_remove {
workspace_rules.remove(&rule);
}
}
SocketMessage::ClearNamedWorkspaceRules(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
let mut workspace_rules = WORKSPACE_RULES.lock();
let mut to_remove = vec![];
for (id, (m_idx, w_idx, _)) in workspace_rules.iter() {
if monitor_idx == *m_idx && workspace_idx == *w_idx {
to_remove.push(id.clone());
}
}
for rule in to_remove {
workspace_rules.remove(&rule);
}
}
}
SocketMessage::ClearAllWorkspaceRules => {
let mut workspace_rules = WORKSPACE_RULES.lock();
workspace_rules.clear();
}
SocketMessage::ManageRule(identifier, ref id) => {
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
@@ -503,9 +455,6 @@ impl WindowManager {
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
}
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
}
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
@@ -528,15 +477,6 @@ impl WindowManager {
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
self.move_workspace_to_monitor(monitor_idx)?;
}
SocketMessage::CycleMoveWorkspaceToMonitor(direction) => {
let monitor_idx = direction.next_idx(
self.focused_monitor_idx(),
NonZeroUsize::new(self.monitors().len())
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.move_workspace_to_monitor(monitor_idx)?;
}
SocketMessage::TogglePause => {
if self.is_paused {
tracing::info!("resuming");
@@ -558,17 +498,13 @@ impl WindowManager {
);
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
self.update_focused_workspace(self.mouse_follows_focus)?;
}
SocketMessage::FocusMonitorNumber(monitor_idx) => {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::Retile => {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
border_manager::destroy_all_borders()?;
self.retile_all(false)?
self.update_focused_workspace(self.mouse_follows_focus)?;
}
SocketMessage::Retile => self.retile_all(false)?,
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
@@ -691,6 +627,10 @@ impl WindowManager {
);
self.focus_workspace(workspace_idx)?;
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::FocusLastWorkspace => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
@@ -714,6 +654,10 @@ impl WindowManager {
self.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?
.set_last_focused_workspace(Option::from(idx));
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
@@ -723,9 +667,11 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
}
if self.focused_workspace_idx().unwrap_or_default() != workspace_idx {
self.focus_workspace(workspace_idx)?;
}
self.focus_workspace(workspace_idx)?;
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::FocusWorkspaceNumbers(workspace_idx) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
@@ -745,17 +691,14 @@ impl WindowManager {
}
self.focus_workspace(workspace_idx)?;
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
if focused_pair != (monitor_idx, workspace_idx) {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
SocketMessage::FocusNamedWorkspace(ref name) => {
if let Some((monitor_idx, workspace_idx)) =
@@ -764,6 +707,10 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::Stop => {
tracing::info!(
@@ -818,46 +765,30 @@ impl WindowManager {
tracing::info!("replying to state done");
}
SocketMessage::GlobalState => {
let state = match serde_json::to_string_pretty(&GlobalState::default()) {
Ok(state) => state,
Err(error) => error.to_string(),
};
tracing::info!("replying to global state");
reply.write_all(state.as_bytes())?;
tracing::info!("replying to global state done");
}
SocketMessage::VisibleWindows => {
let mut monitor_visible_windows = HashMap::new();
for monitor in self.monitors() {
for (index, monitor) in self.monitors().iter().enumerate() {
if let Some(ws) = monitor.focused_workspace() {
monitor_visible_windows.insert(
monitor.device_id().clone(),
monitor
.device_id()
.clone()
.unwrap_or_else(|| format!("{index}")),
ws.visible_window_details().clone(),
);
}
}
let visible_windows_state = serde_json::to_string_pretty(&monitor_visible_windows)
.unwrap_or_else(|error| error.to_string());
let visible_windows_state =
match serde_json::to_string_pretty(&monitor_visible_windows) {
Ok(state) => state,
Err(error) => error.to_string(),
};
reply.write_all(visible_windows_state.as_bytes())?;
}
SocketMessage::MonitorInformation => {
let mut monitors = HashMap::new();
for monitor in self.monitors() {
monitors.insert(monitor.device_id(), monitor.size());
}
let monitors_state = serde_json::to_string_pretty(&monitors)
.unwrap_or_else(|error| error.to_string());
reply.write_all(monitors_state.as_bytes())?;
}
SocketMessage::Query(query) => {
let response = match query {
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
@@ -982,7 +913,7 @@ impl WindowManager {
}
}
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
if !CUSTOM_FFM.load(Ordering::SeqCst) {
@@ -1088,7 +1019,7 @@ impl WindowManager {
SocketMessage::CompleteConfiguration => {
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
}
SocketMessage::WatchConfiguration(enable) => {
@@ -1195,7 +1126,7 @@ impl WindowManager {
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
SocketMessage::Save(ref path) => {
let workspace = self.focused_workspace_mut()?;
@@ -1218,7 +1149,7 @@ impl WindowManager {
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
SocketMessage::AddSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
@@ -1273,7 +1204,6 @@ impl WindowManager {
MoveBehaviour::Insert => {
self.cross_monitor_move_behaviour = MoveBehaviour::Swap;
}
_ => {}
}
}
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {
@@ -1282,99 +1212,42 @@ impl WindowManager {
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
self.unmanaged_window_operation_behaviour = behaviour;
}
SocketMessage::Border(enable) => {
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::BorderImplementation(implementation) => {
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
tracing::error!(
"BorderImplementation::Windows is only supported on Windows 11 and above"
);
} else {
IMPLEMENTATION.store(implementation);
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
self.remove_all_accents()?;
}
BorderImplementation::Windows => {
border_manager::destroy_all_borders()?;
}
SocketMessage::ActiveWindowBorder(enable) => {
if enable {
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
Border::create("komorebi-border-window")?;
}
border_manager::send_notification();
BORDER_ENABLED.store(true, Ordering::SeqCst);
self.show_border()?;
} else {
BORDER_ENABLED.store(false, Ordering::SeqCst);
self.hide_border()?;
}
}
SocketMessage::BorderColour(kind, r, g, b) => match kind {
WindowKind::Single => {
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => {
match kind {
WindowKind::Single => {
BORDER_COLOUR_SINGLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Stack => {
BORDER_COLOUR_STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Monocle => {
BORDER_COLOUR_MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
}
WindowKind::Stack => {
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Monocle => {
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Unfocused => {
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
},
SocketMessage::BorderStyle(style) => {
STYLE.store(style);
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::BorderWidth(width) => {
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
SocketMessage::ActiveWindowBorderWidth(width) => {
BORDER_WIDTH.store(width, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::BorderOffset(offset) => {
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
}
SocketMessage::Animation(enable) => {
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::AnimationDuration(duration) => {
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
}
SocketMessage::AnimationFps(fps) => {
ANIMATION_FPS.store(fps, Ordering::SeqCst);
}
SocketMessage::AnimationStyle(style) => {
*ANIMATION_STYLE.lock() = style;
}
SocketMessage::Transparency(enable) => {
transparency_manager::TRANSPARENCY_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::TransparencyAlpha(alpha) => {
transparency_manager::TRANSPARENCY_ALPHA.store(alpha, Ordering::SeqCst);
}
SocketMessage::StackbarMode(mode) => {
STACKBAR_MODE.store(mode);
}
SocketMessage::StackbarLabel(label) => {
STACKBAR_LABEL.store(label);
}
SocketMessage::StackbarFocusedTextColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_FOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarUnfocusedTextColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarBackgroundColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_TAB_BACKGROUND_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarHeight(height) => {
STACKBAR_TAB_HEIGHT.store(height, Ordering::SeqCst);
}
SocketMessage::StackbarTabWidth(width) => {
STACKBAR_TAB_WIDTH.store(width, Ordering::SeqCst);
}
SocketMessage::StackbarFontSize(size) => {
STACKBAR_FONT_SIZE.store(size, Ordering::SeqCst);
}
#[allow(clippy::assigning_clones)]
SocketMessage::StackbarFontFamily(ref font_family) => {
*STACKBAR_FONT_FAMILY.lock() = font_family.clone();
SocketMessage::ActiveWindowBorderOffset(offset) => {
BORDER_OFFSET.store(offset, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::ApplicationSpecificConfigurationSchema => {
let asc = schema_for!(Vec<ApplicationConfiguration>);
@@ -1421,30 +1294,133 @@ impl WindowManager {
SocketMessage::ToggleTitleBars => {
let current = REMOVE_TITLEBARS.load(Ordering::SeqCst);
REMOVE_TITLEBARS.store(!current, Ordering::SeqCst);
self.update_focused_workspace(false, false)?;
}
SocketMessage::DebugWindow(hwnd) => {
let window = Window::from(hwnd);
let mut rule_debug = RuleDebug::default();
let _ = window.should_manage(None, &mut rule_debug);
let schema = serde_json::to_string_pretty(&rule_debug)?;
reply.write_all(schema.as_bytes())?;
self.update_focused_workspace(false)?;
}
// Deprecated commands
SocketMessage::AltFocusHack(_)
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
};
let notification = Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
};
match message {
SocketMessage::ToggleMonocle => {
let current = BORDER_COLOUR_CURRENT.load(Ordering::SeqCst);
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification();
transparency_manager::send_notification();
stackbar_manager::send_notification();
if monocle != 0 {
if current == monocle {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
} else {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
}
}
}
SocketMessage::StackWindow(_) => {
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
if stack != 0 {
BORDER_COLOUR_CURRENT
.store(BORDER_COLOUR_STACK.load(Ordering::SeqCst), Ordering::SeqCst);
}
}
SocketMessage::UnstackWindow => {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
}
_ => {}
}
match message {
SocketMessage::ChangeLayout(_)
| SocketMessage::CycleLayout(_)
| SocketMessage::ChangeLayoutCustom(_)
| SocketMessage::FlipLayout(_)
| SocketMessage::ManageFocusedWindow
| SocketMessage::MoveWorkspaceToMonitorNumber(_)
| SocketMessage::MoveContainerToMonitorNumber(_)
| SocketMessage::MoveContainerToWorkspaceNumber(_)
| SocketMessage::ResizeWindowEdge(_, _)
| SocketMessage::ResizeWindowAxis(_, _)
| SocketMessage::ToggleFloat
| SocketMessage::ToggleMonocle
| SocketMessage::ToggleMaximize
| SocketMessage::Promote
| SocketMessage::PromoteFocus
| SocketMessage::StackWindow(_)
| SocketMessage::UnstackWindow
| SocketMessage::Retile
// Adding this one so that changes can be seen instantly after
// modifying the active window border offset
| SocketMessage::ActiveWindowBorderOffset(_)
// Adding this one because sometimes EVENT_SYSTEM_FOREGROUND isn't
// getting sent on FocusWindow, meaning the border won't be set
// when processing events
| SocketMessage::FocusWindow(_)
| SocketMessage::InvisibleBorders(_)
| SocketMessage::WorkAreaOffset(_)
| SocketMessage::CycleMoveWindow(_)
| SocketMessage::MoveWindow(_)
| SocketMessage::CycleFocusMonitor(_)
| SocketMessage::CycleFocusWorkspace(_)
| SocketMessage::FocusMonitorNumber(_)
| SocketMessage::FocusMonitorWorkspaceNumber(_, _)
| SocketMessage::FocusWorkspaceNumber(_) => {
let foreground = WindowsApi::foreground_window()?;
let foreground_window = Window { hwnd: foreground };
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
if monocle != 0 && self.focused_workspace()?.monocle_container().is_some() {
BORDER_COLOUR_CURRENT.store(
monocle,
Ordering::SeqCst,
);
}
// it is not acceptable to fail here; we need to be able to send the event to
// subscribers
if self.focused_container().is_ok() {
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
if stack != 0 && self.focused_container()?.windows().len() > 1 {
BORDER_COLOUR_CURRENT
.store(stack, Ordering::SeqCst);
}
}
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.set_position(foreground_window, false)?;
}
SocketMessage::TogglePause => {
let is_paused = self.is_paused;
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
if is_paused {
border.hide()?;
} else {
let focused = self.focused_window()?;
border.set_position(*focused, true)?;
focused.focus(false)?;
}
}
SocketMessage::ToggleTiling | SocketMessage::WorkspaceTiling(..) => {
let tiling_enabled = *self.focused_workspace_mut()?.tile();
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
if tiling_enabled {
let focused = self.focused_window()?;
border.set_position(*focused, true)?;
focused.focus(false)?;
} else {
border.hide()?;
}
}
_ => {}
};
tracing::info!("processed");
Ok(())
@@ -1509,10 +1485,9 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
if wm.is_paused {
return match message {
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message, &mut stream)?)
}
_ => {
tracing::trace!("ignoring while paused");
Ok(())
@@ -1521,6 +1496,10 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
}
wm.process_command(message.clone(), &mut stream)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: wm.as_ref().into(),
})?)?;
}
Ok(())
@@ -1555,10 +1534,9 @@ pub fn read_commands_tcp(
if wm.is_paused {
return match message {
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, stream)?),
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message, stream)?)
}
_ => {
tracing::trace!("ignoring while paused");
Ok(())
@@ -1567,6 +1545,10 @@ pub fn read_commands_tcp(
}
wm.process_command(message.clone(), &mut *stream)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: wm.as_ref().into(),
})?)?;
}
}
}

View File

@@ -1,12 +1,10 @@
use std::fs::OpenOptions;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_utils::atomic::AtomicConsume;
use crossbeam_channel::select;
use parking_lot::Mutex;
use komorebi_core::OperationDirection;
@@ -14,24 +12,24 @@ use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::WindowContainerBehaviour;
use crate::border_manager;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border::Border;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::stackbar_manager;
use crate::transparency_manager;
use crate::window::should_act;
use crate::window::RuleDebug;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::workspace_reconciliator;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HIDDEN;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DATA_DIR;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
@@ -44,15 +42,12 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
tracing::info!("listening");
loop {
if let Ok(event) = receiver.recv() {
let mut guard = wm.lock();
match guard.process_event(event) {
Ok(()) => {}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
select! {
recv(receiver) -> mut maybe_event => {
if let Ok(event) = maybe_event.as_mut() {
match wm.lock().process_event(event) {
Ok(()) => {},
Err(error) => tracing::error!("{}", error)
}
}
}
@@ -64,47 +59,12 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
impl WindowManager {
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[tracing::instrument(skip(self))]
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
pub fn process_event(&mut self, event: &mut WindowManagerEvent) -> Result<()> {
if self.is_paused {
tracing::trace!("ignoring while paused");
return Ok(());
}
let mut rule_debug = RuleDebug::default();
let should_manage = event.window().should_manage(Some(event), &mut rule_debug)?;
// All event handlers below this point should only be processed if the event is
// related to a window that should be managed by the WindowManager.
if !should_manage {
let mut transparency_override = false;
if transparency_manager::TRANSPARENCY_ENABLED.load_consume() {
for m in self.monitors() {
for w in m.workspaces() {
let event_hwnd = event.window().hwnd;
let visible_hwnds = w
.visible_windows()
.iter()
.flatten()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
if w.contains_managed_window(event_hwnd)
&& !visible_hwnds.contains(&event_hwnd)
{
transparency_override = true;
}
}
}
}
if !transparency_override {
return Ok(());
}
}
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
if let Some(id) = current_virtual_desktop() {
if id != *virtual_desktop_id {
@@ -121,38 +81,57 @@ impl WindowManager {
match event {
WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::DisplayChange(window)
| WindowManagerEvent::MoveResizeEnd(_, window) => {
if let Some(monitor_idx) = self.monitor_idx_from_window(window) {
// This is a hidden window apparently associated with COM support mechanisms (based
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
//
// The hidden window, OLEChannelWnd, associated with this class (spawned by
// explorer.exe), after some debugging, is observed to always be tied to the primary
// display monitor, or (usually) monitor 0 in the WindowManager state.
//
// Due to this, at least one user in the Discord has witnessed behaviour where, when
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
// set repeatedly to 0, regardless of where the current foreground window is actually
// located.
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if let Ok(class) = window.class() {
if class != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
}
}
self.reconcile_monitors()?;
let monitor_idx = self.monitor_idx_from_window(*window)
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
// This is a hidden window apparently associated with COM support mechanisms (based
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
//
// The hidden window, OLEChannelWnd, associated with this class (spawned by
// explorer.exe), after some debugging, is observed to always be tied to the primary
// display monitor, or (usually) monitor 0 in the WindowManager state.
//
// Due to this, at least one user in the Discord has witnessed behaviour where, when
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
// set repeatedly to 0, regardless of where the current foreground window is actually
// located.
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if window.class()? != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
}
}
_ => {}
}
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 offset = self.work_area_offset;
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
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)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
@@ -172,14 +151,12 @@ impl WindowManager {
self.has_pending_raise_op = false;
}
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
if self.focused_workspace()?.contains_window(window.hwnd) {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
already_moved_window_handles.remove(&window.hwnd);
}
already_moved_window_handles.remove(&window.hwnd);
}
WindowManagerEvent::Minimize(_, window) => {
let mut hide = false;
@@ -193,7 +170,7 @@ impl WindowManager {
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
}
WindowManagerEvent::Hide(_, window) => {
@@ -222,8 +199,7 @@ impl WindowManager {
path,
&tray_and_multi_window_identifiers,
&regex_identifiers,
)
.is_some();
);
if !window.is_window()
|| should_act
@@ -235,7 +211,7 @@ impl WindowManager {
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
@@ -243,8 +219,6 @@ impl WindowManager {
already_moved_window_handles.remove(&window.hwnd);
}
WindowManagerEvent::FocusChange(_, window) => {
self.update_focused_workspace(self.mouse_follows_focus, false)?;
let workspace = self.focused_workspace_mut()?;
if !workspace
.floating_windows()
@@ -267,51 +241,35 @@ impl WindowManager {
}
}
}
WindowManagerEvent::Show(_, window)
| WindowManagerEvent::Manage(window)
| WindowManagerEvent::Uncloak(_, window) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx =
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let mut needs_reconciliation = false;
WindowManagerEvent::Show(_, window) | WindowManagerEvent::Manage(window) => {
let mut switch_to = None;
for (i, monitors) in self.monitors().iter().enumerate() {
for (j, workspace) in monitors.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
// At this point we know we are going to send a notification to the workspace reconciliator
// So we get the topmost window returned by EnumWindows, which is almost always the window
// that has been selected by alt-tab
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
if let Some(first) =
alt_tab_windows.iter().find(|w| w.title().is_ok())
{
// If our record of this HWND hasn't been updated in over a minute
let mut instant = ALT_TAB_HWND_INSTANT.lock();
if instant.elapsed().gt(&Duration::from_secs(1)) {
// Update our record with the HWND we just found
ALT_TAB_HWND.store(Some(first.hwnd));
// Update the timestamp of our record
*instant = Instant::now();
}
}
}
workspace_reconciliator::send_notification(i, j);
needs_reconciliation = true;
if workspace.contains_window(window.hwnd) {
switch_to = Some((i, j));
}
}
}
if let Some((known_monitor_idx, known_workspace_idx)) = switch_to {
if self.focused_monitor_idx() != known_monitor_idx
|| self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx()
!= known_workspace_idx
{
self.focus_monitor(known_monitor_idx)?;
self.focus_workspace(known_workspace_idx)?;
return Ok(());
}
}
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.container_for_window(window.hwnd).is_some()
@@ -323,48 +281,26 @@ impl WindowManager {
);
window.hide();
proceed = false;
return Ok(());
}
}
}
if proceed {
let behaviour =
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if !workspace_contains_window && !needs_reconciliation {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
stackbar_manager::send_notification();
}
if !workspace.contains_window(window.hwnd) {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(*window);
self.update_focused_workspace(false)?;
}
}
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();
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(*window);
self.update_focused_workspace(true)?;
}
}
}
@@ -400,12 +336,10 @@ impl WindowManager {
.monitor_idx_from_current_pos()
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
let window_container_behaviour =
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
let new_window_behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
let focused_container_idx = workspace.focused_container_idx();
let new_position = WindowsApi::window_rect(window.hwnd())?;
let old_position = *workspace
@@ -461,7 +395,10 @@ impl WindowManager {
// If we have moved across the monitors, use that override, otherwise determine
// if a move has taken place by ruling out a resize
let right_bottom_constant = 0;
let right_bottom_constant = ((BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst))
* 2)
.abs();
let is_move = moved_across_monitors
|| resize.right.abs() == right_bottom_constant
@@ -513,27 +450,26 @@ impl WindowManager {
// the origin monitor's focused workspace
self.focus_monitor(origin_monitor_idx)?;
self.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
self.focus_monitor(target_monitor_idx)?;
self.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
// Here we handle a simple move on the same monitor which is treated as
// a container swap
} else {
match window_container_behaviour {
match new_window_behaviour {
WindowContainerBehaviour::Create => {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace
.swap_containers(focused_container_idx, target_idx);
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
None => {
self.update_focused_workspace(
self.mouse_follows_focus,
false,
)?;
}
}
@@ -542,17 +478,14 @@ impl WindowManager {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace.move_window_to_container(target_idx)?;
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
None => {
self.update_focused_workspace(
self.mouse_follows_focus,
false,
)?;
}
}
stackbar_manager::send_notification();
}
}
}
@@ -596,20 +529,119 @@ impl WindowManager {
self.resize_window(edge, sizing, delta, true)?;
}
self.update_focused_workspace(false, false)?;
self.update_focused_workspace(false)?;
}
}
}
WindowManagerEvent::MouseCapture(..)
WindowManagerEvent::ForceUpdate(_) => {
self.update_focused_workspace(false)?;
}
WindowManagerEvent::DisplayChange(..)
| WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::TitleUpdate(..) => {}
| WindowManagerEvent::Uncloak(..) => {}
};
if !self.focused_workspace()?.tile() {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
if *self.focused_workspace()?.tile() && BORDER_ENABLED.load(Ordering::SeqCst) {
match event {
WindowManagerEvent::MoveResizeStart(_, _) => {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
WindowManagerEvent::MoveResizeEnd(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Hide(_, window)
| WindowManagerEvent::Uncloak(_, window)
| WindowManagerEvent::Minimize(_, window) => {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
let mut target_window = None;
let mut target_window_is_monocle = false;
if self
.focused_workspace()?
.floating_windows()
.iter()
.any(|w| w.hwnd == window.hwnd)
{
target_window = Option::from(*window);
WindowsApi::raise_window(border.hwnd())?;
};
if let Some(monocle_container) = self.focused_workspace()?.monocle_container() {
if let Some(window) = monocle_container.focused_window() {
target_window = Option::from(*window);
target_window_is_monocle = true;
}
}
if target_window.is_none() {
match self.focused_container() {
// if there is no focused container, the desktop is empty
Err(..) => {
WindowsApi::hide_border_window(border.hwnd())?;
}
Ok(container) => {
if !(matches!(event, WindowManagerEvent::Minimize(_, _))
&& container.windows().len() == 1)
{
let container_size = self.focused_container()?.windows().len();
target_window = Option::from(*self.focused_window()?);
if target_window_is_monocle {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
} else if container_size > 1 {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_STACK.load(Ordering::SeqCst),
Ordering::SeqCst,
);
} else {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
}
}
}
}
}
if let Some(target_window) = target_window {
let activate = BORDER_HIDDEN.load(Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
border.set_position(target_window, activate)?;
if activate {
BORDER_HIDDEN.store(false, Ordering::SeqCst);
}
}
}
_ => {}
}
}
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
if let WindowManagerEvent::Unmanage(mut window) = event {
if let WindowManagerEvent::Unmanage(window) = event {
window.center(&self.focused_monitor_work_area()?)?;
}
// If there are no more windows on the workspace, we shouldn't show the border window
if self.focused_workspace()?.containers().is_empty() {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
tracing::trace!("updating list of known hwnds");
let mut known_hwnds = vec![];
for monitor in self.monitors() {
@@ -630,27 +662,12 @@ impl WindowManager {
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &known_hwnds)?;
let notification = Notification {
event: NotificationEvent::WindowManager(event),
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::WindowManager(*event),
state: self.as_ref().into(),
};
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification();
transparency_manager::send_notification();
stackbar_manager::send_notification();
// 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());
}
})?)?;
tracing::info!("processed: {}", event.window().to_string());
Ok(())
}
}

View File

@@ -1,66 +0,0 @@
#![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::send_notification();
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
}
}

View File

@@ -4,7 +4,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Ring<T> {
elements: VecDeque<T>,
focused: usize,

294
komorebi/src/stackbar.rs Normal file
View File

@@ -0,0 +1,294 @@
use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use std::time::Duration;
use color_eyre::eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use windows::core::PCWSTR;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
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::DrawTextW;
use windows::Win32::Graphics::Gdi::GetDC;
use windows::Win32::Graphics::Gdi::ReleaseDC;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::SetBkColor;
use windows::Win32::Graphics::Gdi::SetTextColor;
use windows::Win32::Graphics::Gdi::DT_CENTER;
use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS;
use windows::Win32::Graphics::Gdi::DT_SINGLELINE;
use windows::Win32::Graphics::Gdi::DT_VCENTER;
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
use windows::Win32::Graphics::Gdi::FW_BOLD;
use windows::Win32::Graphics::Gdi::LOGFONTW;
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SW_SHOW;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
use komorebi_core::Rect;
use crate::window::Window;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use crate::WindowManagerEvent;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_BY_BAR_HWNDS;
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct Stackbar {
pub(crate) hwnd: isize,
#[serde(skip)]
pub is_cloned: bool,
}
impl Drop for Stackbar {
fn drop(&mut self) {
if !self.is_cloned {
let _ = WindowsApi::close_window(self.hwnd());
}
}
}
impl Clone for Stackbar {
fn clone(&self) -> Self {
Self {
hwnd: self.hwnd,
is_cloned: true,
}
}
}
impl Stackbar {
unsafe extern "system" fn window_proc(
hwnd: HWND,
msg: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
match msg {
WM_LBUTTONDOWN => {
let win_hwnds_by_topbar = WINDOWS_BY_BAR_HWNDS.lock();
if let Some(win_hwnds) = win_hwnds_by_topbar.get(&hwnd.0) {
let x = l_param.0 as i32 & 0xFFFF;
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst);
let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
for (index, win_hwnd) in win_hwnds.iter().enumerate() {
let left = gap + (index as i32 * (width + gap));
let right = left + width;
let top = 0;
let bottom = height;
if x >= left && x <= right && y >= top && y <= bottom {
let window = Window { hwnd: *win_hwnd };
let event_sender = winevent_listener::event_tx();
let _ = event_sender.send(WindowManagerEvent::FocusChange(
WinEvent::ObjectFocus,
window,
));
let _ = event_sender.send(WindowManagerEvent::ForceUpdate(window));
}
}
}
WINDOWS_BY_BAR_HWNDS.force_unlock();
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, w_param, l_param),
}
}
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create() -> Result<Stackbar> {
let name: Vec<u16> = "komorebi_stackbar\0".encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let wnd_class = WNDCLASSW {
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::window_proc),
hInstance: h_module.into(),
lpszClassName: class_name,
hbrBackground: WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR),
..Default::default()
};
unsafe {
RegisterClassW(&wnd_class);
}
let (hwnd_sender, hwnd_receiver) = crossbeam_channel::bounded::<HWND>(1);
let name_cl = name.clone();
std::thread::spawn(move || -> Result<()> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
PCWSTR(name_cl.as_ptr()),
PCWSTR(name_cl.as_ptr()),
WS_POPUP | WS_VISIBLE,
0,
0,
0,
0,
None,
None,
h_module,
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
hwnd_sender.send(hwnd)?;
let mut msg = MSG::default();
while GetMessageW(&mut msg, hwnd, 0, 0).into() {
TranslateMessage(&msg);
DispatchMessageW(&msg);
std::thread::sleep(Duration::from_millis(10));
}
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?.0,
..Default::default()
})
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
WindowsApi::position_window(self.hwnd(), layout, top)
}
pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect {
Rect {
bottom: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
..*layout
}
}
pub fn update(&self, windows: &VecDeque<Window>, focused_hwnd: isize) -> Result<()> {
let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst);
let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
let background = STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst);
let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst);
let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst);
unsafe {
let hdc = GetDC(self.hwnd());
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
let hbrush = CreateSolidBrush(COLORREF(background));
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
SetBkColor(hdc, COLORREF(background));
let hfont = CreateFontIndirectW(&LOGFONTW {
lfWeight: FW_BOLD.0 as i32,
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
..Default::default()
});
SelectObject(hdc, hfont);
for (i, window) in windows.iter().enumerate() {
if window.hwnd == focused_hwnd {
SetTextColor(hdc, COLORREF(focused_text_colour));
window.focus(false)?;
} else {
SetTextColor(hdc, COLORREF(unfocused_text_colour));
}
let left = gap + (i as i32 * (width + gap));
let mut tab_box = Rect {
top: 0,
left,
right: left + width,
bottom: height,
};
WindowsApi::round_rect(hdc, &tab_box, 8);
let exe = window.exe()?;
let exe_trimmed = exe.trim_end_matches(".exe");
let mut tab_title: Vec<u16> = exe_trimmed.encode_utf16().collect();
tab_box.left_padding(10);
tab_box.right_padding(10);
DrawTextW(
hdc,
&mut tab_title,
&mut tab_box.into(),
DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS,
);
}
ReleaseDC(self.hwnd(), hdc);
}
let mut windows_hwdns: VecDeque<isize> = VecDeque::new();
for window in windows {
windows_hwdns.push_back(window.hwnd);
}
WINDOWS_BY_BAR_HWNDS.lock().insert(self.hwnd, windows_hwdns);
Ok(())
}
pub fn hide(&self) {
WindowsApi::hide_window(self.hwnd())
}
pub fn restore(&self) {
WindowsApi::show_window(self.hwnd(), SW_SHOW)
}
}

View File

@@ -1,224 +0,0 @@
mod stackbar;
use crate::container::Container;
use crate::stackbar_manager::stackbar::Stackbar;
use crate::WindowManager;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
pub static STACKBAR_FONT_SIZE: AtomicI32 = AtomicI32::new(0); // 0 will produce the system default
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
lazy_static! {
pub static ref STACKBAR_STATE: Mutex<HashMap<String, Stackbar>> = Mutex::new(HashMap::new());
pub static ref STACKBAR_FONT_FAMILY: Mutex<Option<String>> = Mutex::new(None);
static ref STACKBARS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref STACKBARS_CONTAINERS: Mutex<HashMap<isize, Container>> = Mutex::new(HashMap::new());
}
pub struct Notification;
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification() {
if event_tx().try_send(Notification).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn should_have_stackbar(window_count: usize) -> bool {
match STACKBAR_MODE.load() {
StackbarMode::Always => true,
StackbarMode::OnStack => window_count > 1,
StackbarMode::Never => false,
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
'receiver: for _ in receiver {
let mut stackbars = STACKBAR_STATE.lock();
let mut stackbars_monitors = STACKBARS_MONITORS.lock();
// Check the wm state every time we receive a notification
let mut state = wm.lock();
// If stackbars are disabled
if matches!(STACKBAR_MODE.load(), StackbarMode::Never)
|| STACKBAR_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
{
for (_, stackbar) in stackbars.iter() {
stackbar.destroy()?;
}
stackbars.clear();
continue 'receiver;
}
for (monitor_idx, m) in state.monitors_mut().iter_mut().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace_mut() {
// Workspaces with tiling disabled don't have stackbars
if !ws.tile() {
let mut to_remove = vec![];
for (id, border) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
continue 'receiver;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
// Handle the monocle container separately
if ws.monocle_container().is_some() || is_maximized {
// Destroy any stackbars associated with the focused workspace
let mut to_remove = vec![];
for (id, stackbar) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
stackbar.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
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());
'containers: for container in ws.containers_mut() {
let should_add_stackbar = match STACKBAR_MODE.load() {
StackbarMode::Always => true,
StackbarMode::OnStack => container.windows().len() > 1,
StackbarMode::Never => false,
};
if !should_add_stackbar {
if let Some(stackbar) = stackbars.get(container.id()) {
stackbar.destroy()?
}
stackbars.remove(container.id());
stackbars_monitors.remove(container.id());
continue 'containers;
}
// Get the stackbar entry for this container from the map or create one
let stackbar = match stackbars.entry(container.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(stackbar) = Stackbar::create(container.id()) {
entry.insert(stackbar)
} else {
continue 'receiver;
}
}
};
stackbars_monitors.insert(container.id().clone(), monitor_idx);
let rect = WindowsApi::window_rect(
container
.focused_window()
.copied()
.unwrap_or_default()
.hwnd(),
)?;
stackbar.update(container_padding, container, &rect)?;
}
}
}
}
Ok(())
}

View File

@@ -1,370 +0,0 @@
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::STYLE;
use crate::container::Container;
use crate::stackbar_manager::STACKBARS_CONTAINERS;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::WINDOWS_11;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::BorderStyle;
use komorebi_core::Rect;
use komorebi_core::StackbarLabel;
use std::os::windows::ffi::OsStrExt;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
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::GetDeviceCaps;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::ReleaseDC;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::SetBkColor;
use windows::Win32::Graphics::Gdi::SetTextColor;
use windows::Win32::Graphics::Gdi::DT_CENTER;
use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS;
use windows::Win32::Graphics::Gdi::DT_SINGLELINE;
use windows::Win32::Graphics::Gdi::DT_VCENTER;
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
use windows::Win32::Graphics::Gdi::FW_BOLD;
use windows::Win32::Graphics::Gdi::LOGFONTW;
use windows::Win32::Graphics::Gdi::LOGPIXELSY;
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::System::WindowsProgramming::MulDiv;
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
#[derive(Debug)]
pub struct Stackbar {
pub hwnd: isize,
}
impl From<isize> for Stackbar {
fn from(value: isize) -> Self {
Self { hwnd: value }
}
}
impl Stackbar {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komostackbar-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let window_class = WNDCLASSW {
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hInstance: h_module.into(),
lpszClassName: class_name,
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let name_cl = name.clone();
std::thread::spawn(move || -> color_eyre::Result<()> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
PCWSTR(name_cl.as_ptr()),
PCWSTR(name_cl.as_ptr()),
WS_POPUP | WS_VISIBLE,
0,
0,
0,
0,
None,
None,
h_module,
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd_sender.send(hwnd)?;
let mut msg: MSG = MSG::default();
loop {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
tracing::debug!("stackbar window event processing thread shutdown");
break;
};
TranslateMessage(&msg);
DispatchMessageW(&msg);
std::thread::sleep(Duration::from_millis(10))
}
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?.0,
})
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::close_window(self.hwnd())
}
pub fn update(
&self,
container_padding: i32,
container: &mut Container,
layout: &Rect,
) -> color_eyre::Result<()> {
let width = STACKBAR_TAB_WIDTH.load_consume();
let height = STACKBAR_TAB_HEIGHT.load_consume();
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
let background = STACKBAR_TAB_BACKGROUND_COLOUR.load_consume();
let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load_consume();
let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load_consume();
let mut stackbars_containers = STACKBARS_CONTAINERS.lock();
stackbars_containers.insert(self.hwnd, container.clone());
let mut layout = *layout;
let workspace_specific_offset =
BORDER_WIDTH.load_consume() + BORDER_OFFSET.load_consume() + container_padding;
layout.top -= workspace_specific_offset + STACKBAR_TAB_HEIGHT.load_consume();
layout.left -= workspace_specific_offset;
WindowsApi::position_window(self.hwnd(), &layout, false)?;
unsafe {
let hdc = GetDC(self.hwnd());
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
let hbrush = CreateSolidBrush(COLORREF(background));
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
SetBkColor(hdc, COLORREF(background));
let mut logfont = LOGFONTW {
lfWeight: FW_BOLD.0 as i32,
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
lfFaceName: [0; 32],
..Default::default()
};
if let Some(font_name) = &*STACKBAR_FONT_FAMILY.lock() {
let font = wide_string(font_name);
for (i, &c) in font.iter().enumerate() {
logfont.lfFaceName[i] = c;
}
}
let logical_height = -MulDiv(
STACKBAR_FONT_SIZE.load(Ordering::SeqCst),
72,
GetDeviceCaps(hdc, LOGPIXELSY),
);
logfont.lfHeight = logical_height;
let hfont = CreateFontIndirectW(&logfont);
SelectObject(hdc, hfont);
for (i, window) in container.windows().iter().enumerate() {
if window.hwnd == container.focused_window().copied().unwrap_or_default().hwnd {
SetTextColor(hdc, COLORREF(focused_text_colour));
} else {
SetTextColor(hdc, COLORREF(unfocused_text_colour));
}
let left = gap + (i as i32 * (width + gap));
let mut rect = Rect {
top: 0,
left,
right: left + width,
bottom: height,
};
match STYLE.load() {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
BorderStyle::Rounded => {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
}
BorderStyle::Square => {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
let label = match STACKBAR_LABEL.load() {
StackbarLabel::Process => {
let exe = window.exe()?;
exe.trim_end_matches(".exe").to_string()
}
StackbarLabel::Title => window.title()?,
};
let mut tab_title: Vec<u16> = label.encode_utf16().collect();
rect.left_padding(10);
rect.right_padding(10);
DrawTextW(
hdc,
&mut tab_title,
&mut rect.into(),
DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS,
);
}
ReleaseDC(self.hwnd(), hdc);
DeleteObject(hpen);
DeleteObject(hbrush);
DeleteObject(hfont);
}
Ok(())
}
pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect {
Rect {
bottom: STACKBAR_TAB_HEIGHT.load_consume(),
..*layout
}
}
unsafe extern "system" fn callback(
hwnd: HWND,
msg: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
unsafe {
match msg {
WM_LBUTTONDOWN => {
let stackbars_containers = STACKBARS_CONTAINERS.lock();
if let Some(container) = stackbars_containers.get(&hwnd.0) {
let x = l_param.0 as i32 & 0xFFFF;
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
let width = STACKBAR_TAB_WIDTH.load_consume();
let height = STACKBAR_TAB_HEIGHT.load_consume();
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
let focused_window_idx = container.focused_window_idx();
let focused_window_rect = WindowsApi::window_rect(
container
.focused_window()
.cloned()
.unwrap_or_default()
.hwnd(),
)
.unwrap_or_default();
for (index, window) in container.windows().iter().enumerate() {
let left = gap + (index as i32 * (width + gap));
let right = left + width;
let top = 0;
let bottom = height;
if x >= left && x <= right && y >= top && y <= bottom {
// If we are focusing a window that isn't currently focused in the
// stackbar, make sure we update its location so that it doesn't render
// on top of other tiles before eventually ending up in the correct
// tile
if index != focused_window_idx {
if let Err(err) =
window.set_position(&focused_window_rect, false)
{
tracing::error!(
"stackbar WM_LBUTTONDOWN repositioning error: hwnd {} ({})",
*window,
err
);
}
}
// Restore the window corresponding to the tab we have clicked
window.restore();
if let Err(err) = window.focus(false) {
tracing::error!(
"stackbar WMLBUTTONDOWN focus error: hwnd {} ({})",
*window,
err
);
}
} else {
// Hide any windows in the stack that don't correspond to the window
// we have clicked
window.hide();
}
}
}
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, w_param, l_param),
}
}
}
}
fn wide_string(s: &str) -> Vec<u16> {
std::ffi::OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}

View File

@@ -1,32 +1,21 @@
use crate::border_manager;
use crate::border_manager::ZOrder;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::border::Border;
use crate::colour::Colour;
use crate::current_virtual_desktop;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_MODE;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::transparency_manager;
use crate::window;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
@@ -38,12 +27,14 @@ use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::STACKBAR_MODE;
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
use crate::WORKSPACE_RULES;
use komorebi_core::BorderImplementation;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use color_eyre::Result;
use crossbeam_channel::Receiver;
@@ -56,9 +47,7 @@ use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::resolve_home_path;
use komorebi_core::AnimationStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::BorderStyle;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
@@ -84,18 +73,27 @@ use uds_windows::UnixListener;
use uds_windows::UnixStream;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BorderColours {
pub struct ActiveWindowBorderColours {
/// Border colour when the container contains a single window
pub single: Option<Colour>,
pub single: Colour,
/// Border colour when the container contains multiple windows
pub stack: Option<Colour>,
pub stack: Colour,
/// Border colour when the container is in monocle mode
pub monocle: Option<Colour>,
/// Border colour when the container is unfocused
pub unfocused: Option<Colour>,
pub monocle: Colour,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ActiveWindowBorderStyle {
#[default]
/// Use the system border style
System,
/// Use the Windows 11-style rounded borders
Rounded,
/// Use the Windows 10-style square borders
Square,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceConfig {
/// Name
pub name: String,
@@ -123,9 +121,6 @@ pub struct WorkspaceConfig {
/// Permanent workspace application rules
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_rules: Option<Vec<IdWithIdentifier>>,
/// Apply this monitor's window-based work area offset (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub apply_window_based_work_area_offset: Option<bool>,
}
impl From<&Workspace> for WorkspaceConfig {
@@ -208,24 +203,17 @@ impl From<&Workspace> for WorkspaceConfig {
workspace_padding,
initial_workspace_rules: initial_ws_rules,
workspace_rules: ws_rules,
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Workspace configurations
pub workspaces: Vec<WorkspaceConfig>,
/// Monitor-specific work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub work_area_offset: Option<Rect>,
/// Window based work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_based_work_area_offset: Option<Rect>,
/// Open window limit after which the window based work area offset will no longer be applied (default: 1)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_based_work_area_offset_limit: Option<isize>,
}
impl From<&Monitor> for MonitorConfig {
@@ -238,24 +226,16 @@ impl From<&Monitor> for MonitorConfig {
Self {
workspaces,
work_area_offset: value.work_area_offset(),
window_based_work_area_offset: value.window_based_work_area_offset(),
window_based_work_area_offset_limit: Some(value.window_based_work_area_offset_limit()),
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.28`
/// The `komorebi.json` static configuration file reference for `v0.1.24`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
pub invisible_borders: Option<Rect>,
/// DISCOURAGED: Minimum width for a window to be eligible for tiling
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum_window_width: Option<i32>,
/// DISCOURAGED: Minimum height for a window to be eligible for tiling
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum_window_height: Option<i32>,
/// Delta to resize windows by (default 50)
#[serde(skip_serializing_if = "Option::is_none")]
pub resize_delta: Option<i32>,
@@ -287,28 +267,13 @@ pub struct StaticConfig {
pub border_offset: Option<i32>,
/// Display an active window border (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border")]
pub border: Option<bool>,
pub active_window_border: Option<bool>,
/// Active window border colours for different container types
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_colours")]
pub border_colours: Option<BorderColours>,
pub active_window_border_colours: Option<ActiveWindowBorderColours>,
/// Active window border style (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_style")]
pub border_style: Option<BorderStyle>,
/// Active window border z-order (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>,
/// Display an active window border (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub border_implementation: Option<BorderImplementation>,
/// Add transparency to unfocused windows (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency: Option<bool>,
/// Alpha value for unfocused window transparency [[0-255]] (default: 200)
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency_alpha: Option<u8>,
pub active_window_border_style: Option<ActiveWindowBorderStyle>,
/// Global default workspace padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
pub default_workspace_padding: Option<i32>,
@@ -318,7 +283,7 @@ pub struct StaticConfig {
/// Monitor and workspace configurations
#[serde(skip_serializing_if = "Option::is_none")]
pub monitors: Option<Vec<MonitorConfig>>,
/// Which Windows signal to use when hiding windows (default: Cloak)
/// Which Windows signal to use when hiding windows (default: minimize)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_hiding_behaviour: Option<HidingBehaviour>,
/// Global work area (space used for tiling) offset (default: None)
@@ -348,106 +313,30 @@ pub struct StaticConfig {
/// Set display index preferences
#[serde(skip_serializing_if = "Option::is_none")]
pub display_index_preferences: Option<HashMap<usize, String>>,
/// Stackbar configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar: Option<StackbarConfig>,
/// Animations configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub animation: Option<AnimationsConfig>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct AnimationsConfig {
/// Enable or disable animations (default: false)
enabled: bool,
/// Set the animation duration in ms (default: 250)
duration: Option<u64>,
/// Set the animation style (default: Linear)
style: Option<AnimationStyle>,
/// Set the animation FPS (default: 60)
fps: Option<u64>,
}
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_options = ["invisible_borders"];
let deprecated_variants = vec![
("Hide", "window_hiding_behaviour", "Cloak"),
("Minimize", "window_hiding_behaviour", "Cloak"),
];
for option in deprecated_options {
if raw.contains(option) {
println!(r#""{option}" is deprecated and can be removed"#);
}
}
for (variant, option, recommended) in deprecated_variants {
if raw.contains(option) && raw.contains(variant) {
println!(
r#"The "{variant}" option for "{option}" is deprecated and can be removed or replaced with "{recommended}""#
);
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
Never,
OnStack,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TabsConfig {
/// Width of a stackbar tab
width: Option<i32>,
/// Focused tab text colour
focused_text: Option<Colour>,
/// Unfocused tab text colour
unfocused_text: Option<Colour>,
/// Tab background colour
background: Option<Colour>,
/// Font family
font_family: Option<String>,
/// Font size
font_size: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StackbarConfig {
/// Stackbar height
pub height: Option<i32>,
/// Stackbar height
pub label: Option<StackbarLabel>,
/// Stackbar mode
pub mode: Option<StackbarMode>,
/// Stackbar tab configuration options
pub tabs: Option<TabsConfig>,
height: Option<i32>,
mode: Option<StackbarMode>,
tabs: Option<TabsConfig>,
}
impl From<&WindowManager> for StaticConfig {
@@ -459,8 +348,6 @@ impl From<&WindowManager> for StaticConfig {
}
let mut to_remove = vec![];
let mut to_add_initial = vec![];
let mut to_add_persistent = vec![];
let workspace_rules = WORKSPACE_RULES.lock();
for (m_idx, m) in monitors.iter().enumerate() {
@@ -477,12 +364,6 @@ impl From<&WindowManager> for StaticConfig {
}
}
for (identifier, (monitor_idx, workspace_idx, initial)) in &*workspace_rules {
if *initial && (*monitor_idx == m_idx && *workspace_idx == w_idx) {
to_add_initial.push((m_idx, w_idx, identifier.clone()));
}
}
if let Some(rules) = &w.workspace_rules {
for wsr in rules {
for (identifier, (monitor_idx, workspace_idx, _)) in &*workspace_rules {
@@ -494,69 +375,38 @@ impl From<&WindowManager> for StaticConfig {
}
}
}
for (identifier, (monitor_idx, workspace_idx, initial)) in &*workspace_rules {
if !*initial && (*monitor_idx == m_idx && *workspace_idx == w_idx) {
to_add_persistent.push((m_idx, w_idx, identifier.clone()));
}
}
}
}
for (m_idx, w_idx, id) in to_remove {
if let Some(monitor) = monitors.get_mut(m_idx) {
if let Some(workspace) = monitor.workspaces.get_mut(w_idx) {
if workspace.workspace_rules.is_none() {
workspace.workspace_rules = Some(vec![]);
}
if let Some(rules) = &mut workspace.workspace_rules {
rules.retain(|r| r.id != id);
for (monitor_idx, workspace_idx, id) in &to_add_persistent {
if m_idx == *monitor_idx && w_idx == *workspace_idx {
rules.push(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: id.clone(),
matching_strategy: None,
})
}
}
rules.dedup();
}
if workspace.initial_workspace_rules.is_none() {
workspace.workspace_rules = Some(vec![]);
}
if let Some(rules) = &mut workspace.initial_workspace_rules {
rules.retain(|r| r.id != id);
for (monitor_idx, workspace_idx, id) in &to_add_initial {
if m_idx == *monitor_idx && w_idx == *workspace_idx {
rules.push(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: id.clone(),
matching_strategy: None,
})
}
}
rules.dedup();
}
}
}
}
let border_colours = if border_manager::FOCUSED.load(Ordering::SeqCst) == 0 {
let border_colours = if BORDER_COLOUR_SINGLE.load(Ordering::SeqCst) == 0 {
None
} else {
Option::from(BorderColours {
single: Option::from(Colour::from(border_manager::FOCUSED.load(Ordering::SeqCst))),
stack: Option::from(Colour::from(border_manager::STACK.load(Ordering::SeqCst))),
monocle: Option::from(Colour::from(border_manager::MONOCLE.load(Ordering::SeqCst))),
unfocused: Option::from(Colour::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
)),
Option::from(ActiveWindowBorderColours {
single: Colour::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
stack: Colour::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
}),
monocle: Colour::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
}),
})
};
@@ -568,24 +418,14 @@ impl From<&WindowManager> for StaticConfig {
unmanaged_window_operation_behaviour: Option::from(
value.unmanaged_window_operation_behaviour,
),
minimum_window_height: Some(window::MINIMUM_HEIGHT.load(Ordering::SeqCst)),
minimum_window_width: Some(window::MINIMUM_WIDTH.load(Ordering::SeqCst)),
focus_follows_mouse: value.focus_follows_mouse,
mouse_follows_focus: Option::from(value.mouse_follows_focus),
app_specific_configuration_path: None,
border_width: Option::from(border_manager::BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: Option::from(border_manager::BORDER_OFFSET.load(Ordering::SeqCst)),
border: Option::from(border_manager::BORDER_ENABLED.load(Ordering::SeqCst)),
border_colours,
transparency: Option::from(
transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst),
),
transparency_alpha: Option::from(
transparency_manager::TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
),
border_style: Option::from(STYLE.load()),
border_z_order: Option::from(Z_ORDER.load()),
border_implementation: Option::from(IMPLEMENTATION.load()),
border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: Option::from(BORDER_OFFSET.load(Ordering::SeqCst)),
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
active_window_border_colours: border_colours,
active_window_border_style: Option::from(*ACTIVE_WINDOW_BORDER_STYLE.lock()),
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
),
@@ -604,7 +444,6 @@ impl From<&WindowManager> for StaticConfig {
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
stackbar: None,
animation: None,
}
}
}
@@ -614,12 +453,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.clone_from(monitor_index_preferences);
*preferences = monitor_index_preferences.clone();
}
if let Some(display_index_preferences) = &self.display_index_preferences {
let mut preferences = DISPLAY_INDEX_PREFERENCES.lock();
preferences.clone_from(display_index_preferences);
*preferences = display_index_preferences.clone();
}
if let Some(behaviour) = self.window_hiding_behaviour {
@@ -627,22 +466,6 @@ impl StaticConfig {
*window_hiding_behaviour = behaviour;
}
if let Some(height) = self.minimum_window_height {
window::MINIMUM_HEIGHT.store(height, Ordering::SeqCst);
}
if let Some(width) = self.minimum_window_width {
window::MINIMUM_WIDTH.store(width, Ordering::SeqCst);
}
if let Some(animations) = &self.animation {
ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst);
ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst);
ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst);
let mut animation_style = ANIMATION_STYLE.lock();
*animation_style = animations.style.unwrap_or(AnimationStyle::Linear);
}
if let Some(container) = self.default_container_padding {
DEFAULT_CONTAINER_PADDING.store(container, Ordering::SeqCst);
}
@@ -651,60 +474,26 @@ impl StaticConfig {
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
}
border_manager::BORDER_WIDTH.store(self.border_width.unwrap_or(8), Ordering::SeqCst);
border_manager::BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
self.border_width.map_or_else(
|| {
BORDER_WIDTH.store(8, Ordering::SeqCst);
},
|width| {
BORDER_WIDTH.store(width, Ordering::SeqCst);
},
);
if let Some(enabled) = &self.border {
border_manager::BORDER_ENABLED.store(*enabled, Ordering::SeqCst);
BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
if let Some(colours) = &self.active_window_border_colours {
BORDER_COLOUR_SINGLE.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_STACK.store(u32::from(colours.stack), Ordering::SeqCst);
BORDER_COLOUR_MONOCLE.store(u32::from(colours.monocle), Ordering::SeqCst);
}
if let Some(colours) = &self.border_colours {
if let Some(single) = colours.single {
border_manager::FOCUSED.store(u32::from(single), Ordering::SeqCst);
}
if let Some(stack) = colours.stack {
border_manager::STACK.store(u32::from(stack), Ordering::SeqCst);
}
if let Some(monocle) = colours.monocle {
border_manager::MONOCLE.store(u32::from(monocle), Ordering::SeqCst);
}
if let Some(unfocused) = colours.unfocused {
border_manager::UNFOCUSED.store(u32::from(unfocused), Ordering::SeqCst);
}
}
STYLE.store(self.border_style.unwrap_or_default());
if !*WINDOWS_11
&& matches!(
self.border_implementation.unwrap_or_default(),
BorderImplementation::Windows
)
{
tracing::error!(
"BorderImplementation::Windows is only supported on Windows 11 and above"
);
} else {
IMPLEMENTATION.store(self.border_implementation.unwrap_or_default());
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
border_manager::destroy_all_borders()?;
}
BorderImplementation::Windows => {
// TODO: figure out how to call wm.remove_all_accents here
}
}
border_manager::send_notification();
}
transparency_manager::TRANSPARENCY_ENABLED
.store(self.transparency.unwrap_or(false), Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ALPHA
.store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst);
let active_window_border_style = self.active_window_border_style.unwrap_or_default();
*ACTIVE_WINDOW_BORDER_STYLE.lock() = active_window_border_style;
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
@@ -745,16 +534,10 @@ impl StaticConfig {
if let Some(height) = &stackbar.height {
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
}
if let Some(label) = &stackbar.label {
STACKBAR_LABEL.store(*label);
}
if let Some(mode) = &stackbar.mode {
STACKBAR_MODE.store(*mode);
let mut stackbar_mode = STACKBAR_MODE.lock();
*stackbar_mode = *mode;
}
#[allow(clippy::assigning_clones)]
if let Some(tabs) = &stackbar.tabs {
if let Some(background) = &tabs.background {
STACKBAR_TAB_BACKGROUND_COLOUR.store((*background).into(), Ordering::SeqCst);
@@ -771,9 +554,6 @@ impl StaticConfig {
if let Some(width) = &tabs.width {
STACKBAR_TAB_WIDTH.store(*width, Ordering::SeqCst);
}
STACKBAR_FONT_SIZE.store(tabs.font_size.unwrap_or(0), Ordering::SeqCst);
*STACKBAR_FONT_FAMILY.lock() = tabs.font_family.clone();
}
}
@@ -855,6 +635,7 @@ impl StaticConfig {
let mut wm = WindowManager {
monitors: Ring::default(),
monitor_cache: HashMap::new(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
@@ -912,20 +693,9 @@ impl StaticConfig {
if let Some(monitors) = value.monitors {
for (i, monitor) in monitors.iter().enumerate() {
{
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
if let Some(device_id) = display_index_preferences.get(&i) {
monitor_reconciliator::insert_in_monitor_cache(device_id, monitor.clone());
}
}
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
m.set_work_area_offset(monitor.work_area_offset);
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
m.set_window_based_work_area_offset_limit(
monitor.window_based_work_area_offset_limit.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
ws.load_static_config(
@@ -953,8 +723,13 @@ impl StaticConfig {
}
}
if value.border == Some(true) {
border_manager::BORDER_ENABLED.store(true, Ordering::SeqCst);
if value.active_window_border == Some(true) {
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
Border::create("komorebi-border-window")?;
}
BORDER_ENABLED.store(true, Ordering::SeqCst);
wm.show_border()?;
}
Ok(())
@@ -971,10 +746,6 @@ impl StaticConfig {
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
m.set_work_area_offset(monitor.work_area_offset);
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
m.set_window_based_work_area_offset_limit(
monitor.window_based_work_area_offset_limit.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
ws.load_static_config(
@@ -1002,8 +773,16 @@ impl StaticConfig {
}
}
if let Some(enabled) = value.border {
border_manager::BORDER_ENABLED.store(enabled, Ordering::SeqCst);
if value.active_window_border == Some(true) {
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
Border::create("komorebi-border-window")?;
}
BORDER_ENABLED.store(true, Ordering::SeqCst);
wm.show_border()?;
} else {
BORDER_ENABLED.store(false, Ordering::SeqCst);
wm.hide_border()?;
}
if let Some(val) = value.window_container_behaviour {

View File

@@ -1,6 +1,4 @@
use bitflags::bitflags;
use serde::Deserialize;
use serde::Serialize;
use windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
use windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
@@ -58,7 +56,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
bitflags! {
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct WindowStyle: u32 {
const BORDER = WS_BORDER.0;
const CAPTION = WS_CAPTION.0;
@@ -92,7 +90,7 @@ bitflags! {
// https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
bitflags! {
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct ExtendedWindowStyle: u32 {
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
const APPWINDOW = WS_EX_APPWINDOW.0;

View File

@@ -1,179 +0,0 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use parking_lot::Mutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU8;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
use crate::Window;
use crate::WindowManager;
use crate::WindowsApi;
pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false);
pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200);
static KNOWN_HWNDS: OnceLock<Mutex<Vec<isize>>> = OnceLock::new();
pub struct Notification;
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn known_hwnds() -> Vec<isize> {
let known = KNOWN_HWNDS.get_or_init(|| Mutex::new(Vec::new())).lock();
known.iter().copied().collect()
}
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification() {
if event_tx().try_send(Notification).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
event_tx().send(Notification)?;
'receiver: for _ in receiver {
let known_hwnds = KNOWN_HWNDS.get_or_init(|| Mutex::new(Vec::new()));
if !TRANSPARENCY_ENABLED.load_consume() {
for hwnd in known_hwnds.lock().iter() {
if let Err(error) = Window::from(*hwnd).opaque() {
tracing::error!("failed to make window {hwnd} opaque: {error}")
}
}
continue 'receiver;
}
known_hwnds.lock().clear();
// Check the wm state every time we receive a notification
let state = wm.lock();
let focused_monitor_idx = state.focused_monitor_idx();
'monitors: for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
let focused_workspace_idx = m.focused_workspace_idx();
'workspaces: for (workspace_idx, ws) in m.workspaces().iter().enumerate() {
// Only operate on the focused workspace of each monitor
// Workspaces with tiling disabled don't have transparent windows
if !ws.tile() || workspace_idx != focused_workspace_idx {
for window in ws.visible_windows().iter().flatten() {
if let Err(error) = window.opaque() {
let hwnd = window.hwnd;
tracing::error!("failed to make window {hwnd} opaque: {error}")
}
}
continue 'workspaces;
}
// Monocle container is never transparent
if let Some(monocle) = ws.monocle_container() {
if let Some(window) = monocle.focused_window() {
if let Err(error) = window.opaque() {
let hwnd = window.hwnd;
tracing::error!("failed to make monocle window {hwnd} opaque: {error}")
}
}
continue 'monitors;
}
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
let is_maximized = WindowsApi::is_zoomed(HWND(foreground_hwnd));
if is_maximized {
if let Err(error) = Window::from(foreground_hwnd).opaque() {
let hwnd = foreground_hwnd;
tracing::error!("failed to make maximized window {hwnd} opaque: {error}")
}
continue 'monitors;
}
for (idx, c) in ws.containers().iter().enumerate() {
// Update the transparency for all containers on this workspace
// If the window is not focused on the current workspace, or isn't on the focused monitor
// make it transparent
#[allow(clippy::collapsible_else_if)]
if idx != ws.focused_container_idx() || monitor_idx != focused_monitor_idx {
let focused_window_idx = c.focused_window_idx();
for (window_idx, window) in c.windows().iter().enumerate() {
if window_idx == focused_window_idx {
match window.transparent() {
Err(error) => {
let hwnd = foreground_hwnd;
tracing::error!(
"failed to make unfocused window {hwnd} transparent: {error}"
)
}
Ok(..) => {
known_hwnds.lock().push(window.hwnd);
}
}
} else {
// just in case, this is useful when people are clicking around
// on unfocused stackbar tabs
known_hwnds.lock().push(window.hwnd);
}
}
// Otherwise, make it opaque
} else {
let focused_window_idx = c.focused_window_idx();
for (window_idx, window) in c.windows().iter().enumerate() {
if window_idx != focused_window_idx {
known_hwnds.lock().push(window.hwnd);
} else {
if let Err(error) =
c.focused_window().copied().unwrap_or_default().opaque()
{
let hwnd = foreground_hwnd;
tracing::error!(
"failed to make focused window {hwnd} opaque: {error}"
)
}
}
}
};
}
}
}
}
Ok(())
}

View File

@@ -1,26 +1,20 @@
use crate::border_manager;
use crate::com::SetCloak;
use crate::stackbar_manager;
use crate::ANIMATIONS_IN_PROGRESS;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Write as _;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::time::Duration;
use color_eyre::eyre;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use regex::Regex;
use schemars::JsonSchema;
use serde::ser::Error;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
@@ -31,10 +25,8 @@ use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
use crate::animation::Animation;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::transparency_manager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::FLOAT_IDENTIFIERS;
@@ -47,31 +39,9 @@ use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
pub static MINIMUM_WIDTH: AtomicI32 = AtomicI32::new(0);
pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
pub struct Window {
pub hwnd: isize,
animation: Animation,
}
impl From<isize> for Window {
fn from(value: isize) -> Self {
Self {
hwnd: value,
animation: Animation::new(value),
}
}
}
impl From<HWND> for Window {
fn from(value: HWND) -> Self {
Self {
hwnd: value.0,
animation: Animation::new(value.0),
}
}
pub(crate) hwnd: isize,
}
#[allow(clippy::module_name_repetitions)]
@@ -127,23 +97,24 @@ impl Serialize for Window {
"title",
&self
.title()
.unwrap_or_else(|_| String::from("could not get window title")),
.map_err(|_| S::Error::custom("could not get window title"))?,
)?;
state.serialize_field(
"exe",
&self
.exe()
.unwrap_or_else(|_| String::from("could not get window exe")),
.map_err(|_| S::Error::custom("could not get window exe"))?,
)?;
state.serialize_field(
"class",
&self
.class()
.unwrap_or_else(|_| String::from("could not get window class")),
.map_err(|_| S::Error::custom("could not get window class"))?,
)?;
state.serialize_field(
"rect",
&WindowsApi::window_rect(self.hwnd()).unwrap_or_default(),
&WindowsApi::window_rect(self.hwnd())
.map_err(|_| S::Error::custom("could not get window rect"))?,
)?;
state.end()
}
@@ -169,61 +140,9 @@ impl Window {
)
}
pub fn animate_position(&self, layout: &Rect, top: bool) -> Result<()> {
let hwnd = self.hwnd();
let curr_rect = WindowsApi::window_rect(hwnd).unwrap();
let target_rect = *layout;
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
let mut animation = self.animation;
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
border_manager::send_notification();
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
std::thread::spawn(move || {
animation.animate(duration, |progress: f64| {
let new_rect = Animation::lerp_rect(&curr_rect, &target_rect, progress);
if progress == 1.0 {
WindowsApi::position_window(hwnd, &new_rect, top)?;
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
.store(false, Ordering::SeqCst);
border_manager::send_notification();
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
} else {
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(hwnd, &new_rect, false)?;
// WindowsApi::position_window(hwnd, &new_rect, top)?;
WindowsApi::invalidate_rect(hwnd, None, false);
}
Ok(())
})
});
Ok(())
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
if WindowsApi::window_rect(self.hwnd())?.eq(layout) {
return Ok(());
}
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
self.animate_position(layout, top)
} else {
WindowsApi::position_window(self.hwnd(), layout, top)
}
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
let rect = *layout;
WindowsApi::position_window(self.hwnd(), &rect, top)
}
pub fn is_maximized(self) -> bool {
@@ -234,10 +153,6 @@ impl Window {
WindowsApi::is_iconic(self.hwnd())
}
pub fn is_visible(self) -> bool {
WindowsApi::is_window_visible(self.hwnd())
}
pub fn hide(self) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
@@ -303,25 +218,77 @@ impl Window {
}
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
// If the target window is already focused, do nothing.
if let Ok(ihwnd) = WindowsApi::foreground_window() {
if HWND(ihwnd) == self.hwnd() {
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
}
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
let current_thread_id = WindowsApi::current_thread_id();
return Ok(());
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
// hook has been installed
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not attach to window thread input processing mechanism, but continuing execution of focus(): {}",
error
);
}
}
};
WindowsApi::raise_and_focus_window(self.hwnd())?;
// Raise Window to foreground
let mut foregrounded = false;
let mut tried_resetting_foreground_access = false;
let mut max_attempts = 10;
while !foregrounded && max_attempts > 0 {
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {
foregrounded = true;
}
Err(error) => {
max_attempts -= 1;
tracing::error!(
"could not set as foreground window, but continuing execution of focus(): {}",
error
);
// If this still doesn't work then maybe try https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-locksetforegroundwindow
if !tried_resetting_foreground_access {
let process_id = WindowsApi::current_process_id();
if WindowsApi::allow_set_foreground_window(process_id).is_ok() {
tried_resetting_foreground_access = true;
}
}
}
};
}
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
}
// This isn't really needed when the above command works as expected via AHK
match WindowsApi::set_focus(self.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not set focus, but continuing execution of focus(): {}",
error
);
}
};
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, false) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not detach from window thread input processing mechanism, but continuing execution of focus(): {}",
error
);
}
};
Ok(())
}
@@ -329,10 +296,7 @@ impl Window {
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd(),
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
WindowsApi::set_transparent(self.hwnd())
}
pub fn opaque(self) -> Result<()> {
@@ -341,14 +305,6 @@ impl Window {
self.update_ex_style(&ex_style)
}
pub fn set_accent(self, colour: u32) -> Result<()> {
WindowsApi::set_window_accent(self.hwnd, Some(colour))
}
pub fn remove_accent(self) -> Result<()> {
WindowsApi::set_window_accent(self.hwnd, None)
}
#[allow(dead_code)]
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
@@ -360,12 +316,12 @@ impl Window {
pub fn style(self) -> Result<WindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
Ok(WindowStyle::from_bits_truncate(bits))
WindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
}
pub fn ex_style(self) -> Result<ExtendedWindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
Ok(ExtendedWindowStyle::from_bits_truncate(bits))
ExtendedWindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
}
pub fn title(self) -> Result<String> {
@@ -414,41 +370,18 @@ impl Window {
self.update_style(&style)
}
#[tracing::instrument(fields(exe, title), skip(debug))]
pub fn should_manage(
self,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> Result<bool> {
if !self.is_window() {
return Ok(false);
#[tracing::instrument(fields(exe, title))]
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
if let Some(WindowManagerEvent::DisplayChange(_)) = event {
return Ok(true);
}
debug.is_window = true;
let rect = WindowsApi::window_rect(self.hwnd()).unwrap_or_default();
if rect.right < MINIMUM_WIDTH.load(Ordering::SeqCst) {
return Ok(false);
}
debug.has_minimum_width = true;
if rect.bottom < MINIMUM_HEIGHT.load(Ordering::SeqCst) {
return Ok(false);
}
debug.has_minimum_height = true;
#[allow(clippy::question_mark)]
if self.title().is_err() {
return Ok(false);
}
debug.has_title = true;
let is_cloaked = self.is_cloaked().unwrap_or_default();
debug.is_cloaked = is_cloaked;
let is_cloaked = self.is_cloaked()?;
let mut allow_cloaked = false;
@@ -461,28 +394,13 @@ impl Window {
}
}
debug.allow_cloaked = allow_cloaked;
match (allow_cloaked, is_cloaked) {
// If allowing cloaked windows, we don't need to check the cloaked status
(true, _) |
// If not allowing cloaked windows, we need to ensure the window is not cloaked
(false, false) => {
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (self.title(), self.exe(), self.class(), self.path()) {
debug.title = Some(title.clone());
debug.exe_name = Some(exe_name.clone());
debug.class = Some(class.clone());
debug.path = Some(path.clone());
// calls for styles can fail quite often for events with windows that aren't really "windows"
// since we have moved up calls of should_manage to the beginning of the process_event handler,
// we should handle failures here gracefully to be able to continue the execution of process_event
if let (Ok(style), Ok(ex_style)) = (&self.style(), &self.ex_style()) {
debug.window_style = Some(*style);
debug.extended_window_style = Some(*ex_style);
let eligible = window_is_eligible(self.hwnd, &title, &exe_name, &class, &path, style, ex_style, event, debug);
debug.should_manage = eligible;
return Ok(eligible);
}
return Ok(window_is_eligible(&title, &exe_name, &class, &path, &self.style()?, &self.ex_style()?, event));
}
}
_ => {}
@@ -492,33 +410,7 @@ impl Window {
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct RuleDebug {
pub should_manage: bool,
pub is_window: bool,
pub has_minimum_width: bool,
pub has_minimum_height: bool,
pub has_title: bool,
pub is_cloaked: bool,
pub allow_cloaked: bool,
pub allow_layered_transparency: bool,
pub window_style: Option<WindowStyle>,
pub extended_window_style: Option<ExtendedWindowStyle>,
pub title: Option<String>,
pub exe_name: Option<String>,
pub class: Option<String>,
pub path: Option<String>,
pub matches_permaignore_class: Option<String>,
pub matches_float_identifier: Option<MatchingRule>,
pub matches_managed_override: Option<MatchingRule>,
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
pub matches_no_titlebar: Option<String>,
}
#[allow(clippy::too_many_arguments)]
fn window_is_eligible(
hwnd: isize,
title: &String,
exe_name: &String,
class: &String,
@@ -526,12 +418,10 @@ fn window_is_eligible(
style: &WindowStyle,
ex_style: &ExtendedWindowStyle,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> bool {
{
let permaignore_classes = PERMAIGNORE_CLASSES.lock();
if permaignore_classes.contains(class) {
debug.matches_permaignore_class = Some(class.clone());
return false;
}
}
@@ -539,71 +429,45 @@ fn window_is_eligible(
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let float_identifiers = FLOAT_IDENTIFIERS.lock();
let should_float = if let Some(rule) = should_act(
let should_float = should_act(
title,
exe_name,
class,
path,
&float_identifiers,
&regex_identifiers,
) {
debug.matches_float_identifier = Some(rule);
true
} else {
false
};
);
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
let managed_override = if let Some(rule) = should_act(
let managed_override = should_act(
title,
exe_name,
class,
path,
&manage_identifiers,
&regex_identifiers,
) {
debug.matches_managed_override = Some(rule);
true
} else {
false
};
);
if should_float && !managed_override {
return false;
}
let layered_whitelist = LAYERED_WHITELIST.lock();
let mut allow_layered = if let Some(rule) = should_act(
let allow_layered = should_act(
title,
exe_name,
class,
path,
&layered_whitelist,
&regex_identifiers,
) {
debug.matches_layered_whitelist = Some(rule);
true
} else {
false
};
);
let known_layered_hwnds = transparency_manager::known_hwnds();
allow_layered = if known_layered_hwnds.contains(&hwnd) {
debug.allow_layered_transparency = true;
true
} else {
allow_layered
};
// TODO: might need this for transparency
// let allow_layered = true;
let allow_wsl2_gui = {
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
let allow = wsl2_ui_processes.contains(exe_name);
if allow {
debug.matches_wsl2_gui = Some(exe_name.clone())
}
allow
wsl2_ui_processes.contains(exe_name)
};
let allow_titlebar_removed = {
@@ -645,10 +509,10 @@ pub fn should_act(
path: &str,
identifiers: &[MatchingRule],
regex_identifiers: &HashMap<String, Regex>,
) -> Option<MatchingRule> {
let mut matching_rule = None;
for rule in identifiers {
match rule {
) -> bool {
let mut should_act = false;
for identifier in identifiers {
match identifier {
MatchingRule::Simple(identifier) => {
if should_act_individual(
title,
@@ -658,7 +522,7 @@ pub fn should_act(
identifier,
regex_identifiers,
) {
matching_rule = Some(rule.clone());
should_act = true
};
}
MatchingRule::Composite(identifiers) => {
@@ -675,13 +539,13 @@ pub fn should_act(
}
if composite_results.iter().all(|&x| x) {
matching_rule = Some(rule.clone());
should_act = true;
}
}
}
}
matching_rule
should_act
}
pub fn should_act_individual(

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,8 @@ pub enum WindowManagerEvent {
Manage(Window),
Unmanage(Window),
Raise(Window),
TitleUpdate(WinEvent, Window),
DisplayChange(Window),
ForceUpdate(Window),
}
impl Display for WindowManagerEvent {
@@ -75,8 +76,11 @@ impl Display for WindowManagerEvent {
Self::Raise(window) => {
write!(f, "Raise (Window: {window})")
}
Self::TitleUpdate(winevent, window) => {
write!(f, "TitleUpdate (WinEvent: {winevent}, Window: {window})")
Self::DisplayChange(window) => {
write!(f, "DisplayChange (Window: {window})")
}
Self::ForceUpdate(window) => {
write!(f, "ForceUpdate (Window: {window})")
}
}
}
@@ -97,8 +101,9 @@ impl WindowManagerEvent {
| Self::MouseCapture(_, window)
| Self::Raise(window)
| Self::Manage(window)
| Self::DisplayChange(window)
| Self::Unmanage(window)
| Self::TitleUpdate(_, window) => window,
| Self::ForceUpdate(window) => window,
}
}
@@ -141,20 +146,19 @@ impl WindowManagerEvent {
let class = &window.class().ok()?;
let path = &window.path().ok()?;
let should_trigger_show = should_act(
let should_trigger = should_act(
title,
exe_name,
class,
path,
&object_name_change_on_launch,
&regex_identifiers,
)
.is_some();
);
if should_trigger_show {
if should_trigger {
Option::from(Self::Show(winevent, window))
} else {
Option::from(Self::TitleUpdate(winevent, window))
None
}
}
_ => None,

View File

@@ -1,12 +1,12 @@
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::Ordering;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::eyre::Error;
use color_eyre::Result;
use widestring::U16CStr;
use windows::core::Result as WindowsCrateResult;
use windows::core::PCWSTR;
use windows::core::PWSTR;
@@ -18,13 +18,10 @@ use windows::Win32::Foundation::HMODULE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::POINT;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
use windows::Win32::Graphics::Dwm::DWMWA_BORDER_COLOR;
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use windows::Win32::Graphics::Dwm::DWMWA_COLOR_NONE;
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
@@ -33,6 +30,7 @@ use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::EnumDisplayDevicesW;
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use windows::Win32::Graphics::Gdi::InvalidateRect;
@@ -40,6 +38,7 @@ use windows::Win32::Graphics::Gdi::MonitorFromPoint;
use windows::Win32::Graphics::Gdi::MonitorFromWindow;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::DISPLAY_DEVICEW;
use windows::Win32::Graphics::Gdi::HBRUSH;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
@@ -48,8 +47,9 @@ use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
use windows::Win32::System::RemoteDesktop::WTSRegisterSessionNotification;
use windows::Win32::System::Threading::AttachThreadInput;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::System::Threading::OpenProcess;
use windows::Win32::System::Threading::QueryFullProcessImageNameW;
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
@@ -61,6 +61,7 @@ use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::HiDpi::MDT_EFFECTIVE_DPI;
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState;
use windows::Win32::UI::Input::KeyboardAndMouse::SendInput;
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_MOUSE;
@@ -86,7 +87,6 @@ use windows::Win32::UI::WindowsAndMessaging::IsIconic;
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
use windows::Win32::UI::WindowsAndMessaging::IsZoomed;
use windows::Win32::UI::WindowsAndMessaging::MoveWindow;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
@@ -99,9 +99,12 @@ use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
use windows::Win32::UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HWND_BOTTOM;
use windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
@@ -112,9 +115,6 @@ use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SPI_GETFOREGROUNDLOCKTIMEOUT;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETFOREGROUNDLOCKTIMEOUT;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
@@ -142,9 +142,8 @@ use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::Window;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::BORDER_HWND;
use crate::TRANSPARENCY_COLOUR;
pub enum WindowsResult<T, E> {
Err(E),
@@ -225,73 +224,58 @@ impl WindowsApi {
}
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
Ok(win32_display_data::connected_displays_all()
.flatten()
.map(|d| {
let name = d.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
let mut monitors: Vec<(String, isize)> = vec![];
let monitors_ref: &mut Vec<(String, isize)> = monitors.as_mut();
Self::enum_display_monitors(
Some(windows_callbacks::valid_display_monitors),
monitors_ref as *mut Vec<(String, isize)> as isize,
)?;
(name, d.hmonitor)
})
.collect::<Vec<_>>())
Ok(monitors)
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
'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);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
Self::enum_display_monitors(
Some(windows_callbacks::enum_display_monitor),
monitors as *mut Ring<Monitor> as isize,
)?;
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
Ok(())
}
for monitor in monitors.elements() {
if device_id.eq(monitor.device_id()) {
continue 'read;
}
}
pub fn enum_display_devices(
index: u32,
lp_device: Option<*const u16>,
) -> Result<DISPLAY_DEVICEW> {
#[allow(clippy::option_if_let_else)]
let lp_device = match lp_device {
None => PCWSTR::null(),
Some(lp_device) => PCWSTR(lp_device),
};
let m = monitor::new(
display.hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
);
let mut display_device = DISPLAY_DEVICEW {
cb: u32::try_from(std::mem::size_of::<DISPLAY_DEVICEW>())?,
..Default::default()
};
let mut index_preference = None;
let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
for (index, monitor_size) in &*monitor_index_preferences {
if m.size() == monitor_size {
index_preference = Option::from(index);
}
}
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, id) in &*display_index_preferences {
if id.eq(m.device_id()) {
index_preference = Option::from(index);
}
}
if monitors.elements().is_empty() {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
while *preference > monitors.elements().len() {
monitors.elements_mut().reserve(1);
}
monitors.elements_mut().insert(*preference, m);
} else {
monitors.elements_mut().push_back(m);
match unsafe {
EnumDisplayDevicesW(
lp_device,
index,
std::ptr::addr_of_mut!(display_device),
EDD_GET_DEVICE_INTERFACE_NAME,
)
}
.ok()
{
Ok(()) => {}
Err(error) => {
tracing::error!("enum_display_devices: {}", error);
return Err(error.into());
}
}
Ok(())
Ok(display_device)
}
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
@@ -363,19 +347,12 @@ impl WindowsApi {
/// the layout to account for any window shadow borders (the window painted
/// region will match layout on completion).
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
let mut flags = SetWindowPosition::NO_ACTIVATE
let flags = SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_COPY_BITS
| SetWindowPosition::FRAME_CHANGED;
// If the request is to place the window on top, then HWND_TOP will take
// effect, otherwise pass NO_Z_ORDER that will cause set_window_pos to
// ignore the z-order paramter.
if !top {
flags |= SetWindowPosition::NO_Z_ORDER;
}
let shadow_rect = Self::shadow_rect(hwnd).unwrap_or_default();
let shadow_rect = Self::shadow_rect(hwnd)?;
let rect = Rect {
left: layout.left + shadow_rect.left,
top: layout.top + shadow_rect.top,
@@ -383,39 +360,51 @@ impl WindowsApi {
bottom: layout.bottom + shadow_rect.bottom,
};
// Note: earlier code had set HWND_TOPMOST here, but we should not do
// that. HWND_TOPMOST is a sticky z-order change, rather than a regular
// z-order reordering. Programs will use TOPMOST themselves to do things
// such as making sure that their tool windows or dialog pop-ups are
// above their main window. If any such windows are unmanaged, they must
// still remian topmost, so we set HWND_TOP here, which will cause the
// managed window to come to the front, but if the managed window has a
// child that is TOPMOST it will still be rendered above, in the proper
// order expected by the application. It's also important to understand
// that TOPMOST is somewhat viral, in that when you set a window to
// TOPMOST all of its owned windows are also made TOPMOST.
// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos#remarks
Self::set_window_pos(hwnd, &rect, HWND_TOP, flags.bits())
let position = if top { HWND_TOP } else { HWND_NOTOPMOST };
Self::set_window_pos(hwnd, &rect, position, flags.bits())
}
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
unsafe { BringWindowToTop(hwnd) }.process()
}
// Raise the window to the top of the Z order, but do not activate or focus
// it. Use raise_and_focus_window to activate and focus a window.
pub fn raise_window(hwnd: HWND) -> Result<()> {
let flags = SetWindowPosition::NO_MOVE | SetWindowPosition::NO_ACTIVATE;
let flags = SetWindowPosition::NO_MOVE;
let position = HWND_TOP;
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
}
pub fn set_border_pos(hwnd: HWND, layout: &Rect, position: HWND) -> Result<()> {
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
pub fn position_border_window(hwnd: HWND, layout: &Rect, activate: bool) -> Result<()> {
let flags = if activate {
SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE
} else {
SetWindowPosition::NO_ACTIVATE
};
// TODO(raggi): This leaves the window behind the active window, which
// can result e.g. single pixel window borders being invisible in the
// case of opaque window borders (e.g. EPIC Games Launcher). Ideally
// we'd be able to pass a parent window to place ourselves just in front
// of, however the SetWindowPos API explicitly ignores that parameter
// unless the window being positioned is being activated - and we don't
// want to activate the border window here. We can hopefully find a
// better workaround in the future.
// The trade-off chosen prevents the border window from sitting over the
// top of other pop-up dialogs such as a file picker dialog from
// Firefox. When adjusting this in the future, it's important to check
// those dialog cases.
let position = HWND_NOTOPMOST;
Self::set_window_pos(hwnd, layout, position, flags.bits())
}
pub fn hide_border_window(hwnd: HWND) -> Result<()> {
let flags = SetWindowPosition::HIDE_WINDOW;
let position = HWND_BOTTOM;
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
}
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
unsafe {
@@ -432,17 +421,6 @@ impl WindowsApi {
.process()
}
pub fn move_window(hwnd: HWND, layout: &Rect, repaint: bool) -> Result<()> {
let shadow_rect = Self::shadow_rect(hwnd).unwrap_or_default();
let rect = Rect {
left: layout.left + shadow_rect.left,
top: layout.top + shadow_rect.top,
right: layout.right + shadow_rect.right,
bottom: layout.bottom + shadow_rect.bottom,
};
unsafe { MoveWindow(hwnd, rect.left, rect.top, rect.right, rect.bottom, repaint) }.process()
}
pub fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
// BOOL is returned but does not signify whether or not the operation was succesful
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
@@ -484,31 +462,8 @@ impl WindowsApi {
unsafe { GetForegroundWindow() }.process()
}
pub fn raise_and_focus_window(hwnd: HWND) -> Result<()> {
let event = [INPUT {
r#type: INPUT_MOUSE,
..Default::default()
}];
unsafe {
// Send an input event to our own process first so that we pass the
// foreground lock check
SendInput(&event, size_of::<INPUT>() as i32);
// Error ignored, as the operation is not always necessary.
let _ = SetWindowPos(
hwnd,
HWND_TOP,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
)
.process();
SetForegroundWindow(hwnd)
}
.ok()
.process()
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
unsafe { SetForegroundWindow(hwnd) }.ok().process()
}
#[allow(dead_code)]
@@ -525,16 +480,6 @@ impl WindowsApi {
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
}
pub fn alt_tab_windows() -> Result<Vec<Window>> {
let mut hwnds = vec![];
Self::enum_windows(
Some(windows_callbacks::alt_tab_windows),
&mut hwnds as *mut Vec<Window> as isize,
)?;
Ok(hwnds)
}
#[allow(dead_code)]
pub fn top_visible_window() -> Result<isize> {
let hwnd = Self::top_window()?;
@@ -638,6 +583,10 @@ impl WindowsApi {
(process_id, thread_id)
}
pub fn current_thread_id() -> u32 {
unsafe { GetCurrentThreadId() }
}
pub fn current_process_id() -> u32 {
unsafe { GetCurrentProcessId() }
}
@@ -655,6 +604,16 @@ impl WindowsApi {
}
}
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
.ok()
.process()
}
pub fn set_focus(hwnd: HWND) -> Result<()> {
unsafe { SetFocus(hwnd) }.process().map(|_| ())
}
#[allow(dead_code)]
fn set_window_long_ptr_w(
hwnd: HWND,
@@ -806,32 +765,20 @@ impl WindowsApi {
}
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
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();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let ex_info = Self::monitor_info_w(HMONITOR(hmonitor))?;
let name = U16CStr::from_slice_truncate(&ex_info.szDevice)
.expect("monitor name was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\.\")
.to_string();
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
let monitor = monitor::new(
hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
);
return Ok(monitor);
}
}
bail!("could not find device_id for hmonitor: {hmonitor}");
Ok(monitor::new(
hmonitor,
ex_info.monitorInfo.rcMonitor.into(),
ex_info.monitorInfo.rcWork.into(),
name,
))
}
pub fn set_process_dpi_awareness_context() -> Result<()> {
@@ -971,19 +918,6 @@ impl WindowsApi {
.process()
}
pub fn set_window_accent(hwnd: isize, color: Option<u32>) -> Result<()> {
let col_ref = COLORREF(color.unwrap_or(DWMWA_COLOR_NONE));
unsafe {
DwmSetWindowAttribute(
HWND(hwnd),
DWMWA_BORDER_COLOR,
std::ptr::addr_of!(col_ref).cast(),
4,
)
}
.process()
}
pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
unsafe {
let hwnd = CreateWindowExW(
@@ -1001,17 +935,18 @@ impl WindowsApi {
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
hwnd
}
.process()
}
pub fn set_transparent(hwnd: HWND, alpha: u8) -> Result<()> {
pub fn set_transparent(hwnd: HWND) -> Result<()> {
unsafe {
#[allow(clippy::cast_sign_loss)]
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), alpha, LWA_ALPHA)?;
// TODO: alpha should be configurable
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), 150, LWA_ALPHA)?;
}
Ok(())
@@ -1037,9 +972,10 @@ impl WindowsApi {
.process()
}
pub fn invalidate_rect(hwnd: HWND, rect: Option<&Rect>, erase: bool) -> bool {
let rect = rect.map(|rect| &rect.rect() as *const RECT);
unsafe { InvalidateRect(hwnd, rect, erase) }.as_bool()
pub fn invalidate_border_rect() -> Result<()> {
unsafe { InvalidateRect(HWND(BORDER_HWND.load(Ordering::SeqCst)), None, false) }
.ok()
.process()
}
pub fn alt_is_pressed() -> bool {
@@ -1091,8 +1027,4 @@ impl WindowsApi {
SendInput(&inputs, std::mem::size_of::<INPUT>() as i32)
}
}
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
unsafe { WTSRegisterSessionNotification(HWND(hwnd), 1) }.process()
}
}

View File

@@ -1,17 +1,150 @@
use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use widestring::U16CStr;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::SPI_ICONVERTICALSPACING;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
use crate::container::Container;
use crate::window::RuleDebug;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use crate::ActiveWindowBorderStyle;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_RECT;
use crate::BORDER_WIDTH;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_11;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<(String, isize)>) };
if let Ok(m) = WindowsApi::monitor(hmonitor.0) {
monitors.push((m.name().to_string(), hmonitor.0));
}
true.into()
}
pub extern "system" fn enum_display_monitor(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
// Don't duplicate a monitor that is already being managed
for monitor in monitors.elements() {
if monitor.id() == hmonitor.0 {
return true.into();
}
}
let current_index = monitors.elements().len();
if let Ok(mut m) = WindowsApi::monitor(hmonitor.0) {
#[allow(clippy::cast_possible_truncation)]
if let Ok(d) = WindowsApi::enum_display_devices(current_index as u32, None) {
let name = U16CStr::from_slice_truncate(d.DeviceName.as_ref())
.expect("display device name was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\.\")
.to_string();
if name.eq(m.name()) {
if let Ok(device) = WindowsApi::enum_display_devices(0, Some(d.DeviceName.as_ptr()))
{
let id = U16CStr::from_slice_truncate(device.DeviceID.as_ref())
.expect("display device id was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\?\")
.to_string();
let mut split: Vec<_> = id.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
m.set_device(Option::from(split[0].to_string()));
m.set_device_id(Option::from(split.join("-")));
}
}
}
let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
let mut index_preference = None;
for (index, monitor_size) in &*monitor_index_preferences {
if m.size() == monitor_size {
index_preference = Option::from(index);
}
}
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, device) in &*display_index_preferences {
if let Some(known_device) = m.device_id() {
if device == known_device {
index_preference = Option::from(index);
}
}
}
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 {
monitors.elements_mut().reserve(1);
}
monitors.elements_mut().insert(*preference, m);
} else {
monitors.elements_mut().push_back(m);
}
}
true.into()
}
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
@@ -22,9 +155,9 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let is_maximized = WindowsApi::is_zoomed(hwnd);
if is_visible && is_window && !is_minimized {
let window = Window::from(hwnd);
let window = Window { hwnd: hwnd.0 };
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
if let Ok(should_manage) = window.should_manage(None) {
if should_manage {
if is_maximized {
WindowsApi::restore_window(hwnd);
@@ -40,26 +173,6 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let is_visible = WindowsApi::is_window_visible(hwnd);
let is_window = WindowsApi::is_window(hwnd);
let is_minimized = WindowsApi::is_iconic(hwnd);
if is_visible && is_window && !is_minimized {
let window = Window::from(hwnd);
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
if should_manage {
windows.push(window);
}
}
}
true.into()
}
pub extern "system" fn win_event_hook(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
@@ -74,29 +187,123 @@ pub extern "system" fn win_event_hook(
return;
}
let window = Window::from(hwnd);
let window = Window { hwnd: hwnd.0 };
let winevent = match WinEvent::try_from(event) {
Ok(event) => event,
Err(_) => return,
};
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
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;
}
None => return,
Some(event) => event,
};
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
if should_manage {
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
}
}
pub extern "system" fn border_window(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_PAINT => {
let border_rect = *BORDER_RECT.lock();
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
let hpen = CreatePen(
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(BORDER_COLOUR_CURRENT.load(Ordering::SeqCst)),
);
let hbrush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
// TODO(raggi): this is approximately the correct curvature for
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
// often the bottom right has a different shape. Furthermore if
// the window was made with DWMWCP_ROUNDSMALL then this is the
// wrong size. In the future we should read the DWM properties
// of windows and attempt to match appropriately.
match *ACTIVE_WINDOW_BORDER_STYLE.lock() {
ActiveWindowBorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, border_rect.right, border_rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
}
}
ActiveWindowBorderStyle::Rounded => {
RoundRect(hdc, 0, 0, border_rect.right, border_rect.bottom, 20, 20);
}
ActiveWindowBorderStyle::Square => {
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
}
}
EndPaint(window, &ps);
ValidateRect(window, None);
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
pub extern "system" fn hidden_window(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_DISPLAYCHANGE => {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
LRESULT(0)
}
// Added based on this https://stackoverflow.com/a/33762334
WM_SETTINGCHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == SPI_SETWORKAREA.0
|| wparam.0 as u32 == SPI_ICONVERTICALSPACING.0
{
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}
// Added based on this https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}

View File

@@ -1,5 +1,4 @@
use std::sync::OnceLock;
use std::time::Duration;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
@@ -35,26 +34,23 @@ pub fn start() {
)
};
let mut msg: MSG = MSG::default();
loop {
let mut msg: MSG = MSG::default();
unsafe {
if !GetMessageW(&mut msg, HWND(0), 0, 0).as_bool() {
tracing::debug!("windows event processing thread shutdown");
tracing::info!("windows event processing shutdown");
break;
};
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
std::thread::sleep(Duration::from_millis(10))
}
})
});
}
fn channel() -> &'static (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
CHANNEL.get_or_init(crossbeam_channel::unbounded)
}
pub fn event_tx() -> Sender<WindowManagerEvent> {

View File

@@ -20,34 +20,24 @@ use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::container::Container;
use crate::ring::Ring;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::static_config::WorkspaceConfig;
use crate::window::Window;
use crate::window::WindowDetails;
use crate::windows_api::WindowsApi;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::NO_TITLEBAR;
use crate::REMOVE_TITLEBARS;
use crate::STACKBAR_TAB_HEIGHT;
#[allow(clippy::struct_field_names)]
#[derive(
Debug,
Clone,
Serialize,
Deserialize,
Getters,
CopyGetters,
MutGetters,
Setters,
JsonSchema,
PartialEq,
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Workspace {
#[getset(get = "pub", set = "pub")]
@@ -81,8 +71,6 @@ pub struct Workspace {
resize_dimensions: Vec<Option<Rect>>,
#[getset(get = "pub", set = "pub")]
tile: bool,
#[getset(get_copy = "pub", set = "pub")]
apply_window_based_work_area_offset: bool,
}
impl_ring_elements!(Workspace, Container);
@@ -105,7 +93,6 @@ impl Default for Workspace {
latest_layout: vec![],
resize_dimensions: vec![],
tile: true,
apply_window_based_work_area_offset: true,
}
}
}
@@ -144,8 +131,6 @@ impl Workspace {
}
self.set_layout_rules(all_rules);
self.tile = true;
}
if let Some(layout_rules) = &config.custom_layout_rules {
@@ -154,14 +139,8 @@ impl Workspace {
let rule = CustomLayout::from_path(pathbuf)?;
rules.push((*count, Layout::Custom(rule)));
}
self.tile = true;
}
self.set_apply_window_based_work_area_offset(
config.apply_window_based_work_area_offset.unwrap_or(true),
);
Ok(())
}
@@ -196,14 +175,6 @@ impl Workspace {
}
pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> {
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window() {
container.restore();
window.focus(mouse_follows_focus)?;
return Ok(());
}
}
let idx = self.focused_container_idx();
let mut to_focus = None;
@@ -221,6 +192,10 @@ impl Workspace {
container.restore();
}
if let Some(container) = self.monocle_container_mut() {
container.restore();
}
for window in self.floating_windows() {
window.restore();
}
@@ -240,21 +215,13 @@ impl Workspace {
Ok(())
}
pub fn update(
&mut self,
work_area: &Rect,
work_area_offset: Option<Rect>,
window_based_work_area_offset: (isize, Option<Rect>),
) -> Result<()> {
pub fn update(&mut self, work_area: &Rect, offset: Option<Rect>) -> Result<()> {
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
return Ok(());
}
let (window_based_work_area_offset_limit, window_based_work_area_offset) =
window_based_work_area_offset;
let container_padding = self.container_padding();
let mut adjusted_work_area = work_area_offset.map_or_else(
let mut adjusted_work_area = offset.map_or_else(
|| *work_area,
|offset| {
let mut with_offset = *work_area;
@@ -267,23 +234,6 @@ impl Workspace {
},
);
if self.containers().len() <= window_based_work_area_offset_limit as usize
&& self.apply_window_based_work_area_offset
{
adjusted_work_area = window_based_work_area_offset.map_or_else(
|| adjusted_work_area,
|offset| {
let mut with_offset = adjusted_work_area;
with_offset.left += offset.left;
with_offset.top += offset.top;
with_offset.right -= offset.right;
with_offset.bottom -= offset.bottom;
with_offset
},
);
}
adjusted_work_area.add_padding(self.workspace_padding().unwrap_or_default());
self.enforce_resize_constraints();
@@ -323,7 +273,7 @@ impl Workspace {
} else if let Some(window) = self.maximized_window_mut() {
window.maximize();
} else if !self.containers().is_empty() {
let mut layouts = self.layout().as_boxed_arrangement().calculate(
let layouts = self.layout().as_boxed_arrangement().calculate(
&adjusted_work_area,
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
anyhow!(
@@ -338,14 +288,22 @@ impl Workspace {
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
let no_titlebar = NO_TITLEBAR.lock().clone();
let focused_hwnd = self
.focused_container()
.ok_or_else(|| anyhow!("couldn't find a focused container"))?
.focused_window()
.ok_or_else(|| anyhow!("couldn't find a focused window"))?
.hwnd;
let container_padding = self.container_padding().unwrap_or(0);
let containers = self.containers_mut();
for (i, container) in containers.iter_mut().enumerate() {
let window_count = container.windows().len();
let container_windows = container.windows().clone();
let container_topbar = container.stackbar().clone();
if let (Some(window), Some(layout)) =
(container.focused_window_mut(), layouts.get_mut(i))
(container.focused_window_mut(), layouts.get(i))
{
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
window.remove_title_bar()?;
@@ -359,23 +317,30 @@ impl Workspace {
WindowsApi::restore_window(window.hwnd());
}
let mut rect = *layout;
{
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
layout.add_padding(border_offset);
rect.add_padding(border_offset);
let width = BORDER_WIDTH.load(Ordering::SeqCst);
layout.add_padding(width);
rect.add_padding(width);
}
if stackbar_manager::should_have_stackbar(window_count) {
if let Some(stackbar) = container_topbar {
stackbar.set_position(
&stackbar.get_position_from_container_layout(layout),
false,
)?;
stackbar.update(&container_windows, focused_hwnd)?;
let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
let total_height = tab_height + container_padding;
layout.top += total_height;
layout.bottom -= total_height;
rect.top += total_height;
rect.bottom -= total_height;
}
window.set_position(layout, false)?;
window.set_position(&rect, false)?;
}
}
@@ -392,51 +357,9 @@ impl Workspace {
Ok(())
}
// focus_changed performs updates in response to the fact that a focus
// change event has occurred. The focus change is assumed to be valid, and
// should not result in a new focus change - the intent here is to update
// focus-reactive elements, such as the stackbar.
pub fn focus_changed(&mut self, hwnd: isize) -> Result<()> {
if !self.tile() {
return Ok(());
}
let containers = self.containers_mut();
for container in containers.iter_mut() {
if let Some(idx) = container.idx_for_window(hwnd) {
container.focus_window(idx);
container.restore();
}
}
Ok(())
}
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() {
@@ -471,14 +394,6 @@ 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()))
}
@@ -708,10 +623,6 @@ impl Workspace {
self.set_monocle_container_restore_idx(None);
}
for c in self.containers() {
c.restore();
}
return Ok(());
}
}
@@ -757,9 +668,6 @@ impl Workspace {
self.focus_previous_container();
} else {
container.load_focused_window();
if let Some(window) = container.focused_window() {
window.focus(false)?;
}
}
Ok(())
@@ -814,7 +722,7 @@ impl Workspace {
self.resize_dimensions_mut().remove(focused_idx);
if focused_idx < target_container_idx {
target_container_idx.saturating_sub(1)
target_container_idx - 1
} else {
target_container_idx
}
@@ -937,7 +845,7 @@ impl Workspace {
self.resize_dimensions_mut().remove(focused_idx);
if focused_idx == self.containers().len() {
self.focus_container(focused_idx.saturating_sub(1));
self.focus_container(focused_idx - 1);
}
} else {
container.load_focused_window();
@@ -954,17 +862,6 @@ impl Workspace {
fn enforce_resize_constraints(&mut self) {
match self.layout {
Layout::Default(DefaultLayout::BSP) => self.enforce_resize_constraints_for_bsp(),
Layout::Default(DefaultLayout::Columns) => self.enforce_resize_for_columns(),
Layout::Default(DefaultLayout::Rows) => self.enforce_resize_for_rows(),
Layout::Default(DefaultLayout::VerticalStack) => {
self.enforce_resize_for_vertical_stack();
}
Layout::Default(DefaultLayout::RightMainVerticalStack) => {
self.enforce_resize_for_right_vertical_stack();
}
Layout::Default(DefaultLayout::HorizontalStack) => {
self.enforce_resize_for_horizontal_stack();
}
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
self.enforce_resize_for_ultrawide();
}
@@ -998,146 +895,6 @@ impl Workspace {
}
}
fn enforce_resize_for_columns(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
let len = resize_dimensions.len();
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
if let Some(rect) = rect {
rect.top = 0;
rect.bottom = 0;
if i == 0 {
rect.left = 0;
}
if i == len - 1 {
rect.right = 0;
}
}
}
}
}
}
fn enforce_resize_for_rows(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
let len = resize_dimensions.len();
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
if let Some(rect) = rect {
rect.left = 0;
rect.right = 0;
if i == 0 {
rect.top = 0;
}
if i == len - 1 {
rect.bottom = 0;
}
}
}
}
}
}
fn enforce_resize_for_vertical_stack(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
// Single window can not be resized at all
0 | 1 => self.enforce_no_resize(),
_ => {
// Zero is actually on the left
if let Some(mut left) = resize_dimensions[0] {
left.top = 0;
left.bottom = 0;
left.left = 0;
}
// Handle stack on the right
let stack_size = resize_dimensions[1..].len();
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
if let Some(rect) = rect {
// No containers can resize to the right
rect.right = 0;
// First container in stack cant resize up
if i == 0 {
rect.top = 0;
} else if i == stack_size - 1 {
// Last cant be resized to the bottom
rect.bottom = 0;
}
}
}
}
}
}
fn enforce_resize_for_right_vertical_stack(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
// Single window can not be resized at all
0 | 1 => self.enforce_no_resize(),
_ => {
// Zero is actually on the right
if let Some(mut left) = resize_dimensions[1] {
left.top = 0;
left.bottom = 0;
left.right = 0;
}
// Handle stack on the right
let stack_size = resize_dimensions[1..].len();
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
if let Some(rect) = rect {
// No containers can resize to the left
rect.left = 0;
// First container in stack cant resize up
if i == 0 {
rect.top = 0;
} else if i == stack_size - 1 {
// Last cant be resized to the bottom
rect.bottom = 0;
}
}
}
}
}
}
fn enforce_resize_for_horizontal_stack(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
if let Some(mut left) = resize_dimensions[0] {
left.top = 0;
left.left = 0;
left.right = 0;
}
let stack_size = resize_dimensions[1..].len();
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
if let Some(rect) = rect {
rect.bottom = 0;
if i == 0 {
rect.left = 0;
}
if i == stack_size - 1 {
rect.right = 0;
}
}
}
}
}
}
fn enforce_resize_for_ultrawide(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
@@ -1238,7 +995,7 @@ impl Workspace {
.ok_or_else(|| anyhow!("there is no monocle container"))?;
let container = container.clone();
if restore_idx >= self.containers().len() {
if restore_idx > self.containers().len() - 1 {
self.containers_mut()
.resize(restore_idx, Container::default());
}
@@ -1345,8 +1102,7 @@ impl Workspace {
.ok_or_else(|| anyhow!("there is no monocle container"))?;
let window = *window;
if !self.containers().is_empty() && restore_idx > self.containers().len().saturating_sub(1)
{
if !self.containers().is_empty() && restore_idx > self.containers().len() - 1 {
self.containers_mut()
.resize(restore_idx, Container::default());
}
@@ -1433,12 +1189,15 @@ impl Workspace {
vec
}
pub fn focus_previous_container(&mut self) {
fn focus_previous_container(&mut self) {
let focused_idx = self.focused_container_idx();
self.focus_container(focused_idx.saturating_sub(1));
if focused_idx != 0 {
self.focus_container(focused_idx - 1);
}
}
fn focus_last_container(&mut self) {
self.focus_container(self.containers().len().saturating_sub(1));
self.focus_container(self.containers().len() - 1);
}
}

View File

@@ -1,128 +0,0 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::WindowManager;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone)]
pub struct Notification {
pub monitor_idx: usize,
pub workspace_idx: usize,
}
pub static ALT_TAB_HWND: AtomicCell<Option<isize>> = AtomicCell::new(None);
lazy_static! {
pub static ref ALT_TAB_HWND_INSTANT: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
}
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification(monitor_idx: usize, workspace_idx: usize) {
if event_tx()
.try_send(Notification {
monitor_idx,
workspace_idx,
})
.is_err()
{
tracing::warn!("channel is full; dropping notification")
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
let arc = wm.clone();
for notification in receiver {
tracing::info!("running reconciliation");
let mut wm = wm.lock();
let focused_monitor_idx = wm.focused_monitor_idx();
let focused_workspace_idx =
wm.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let updated_pair = (notification.monitor_idx, notification.workspace_idx);
if focused_pair != updated_pair {
wm.focus_monitor(notification.monitor_idx)?;
let mouse_follows_focus = wm.mouse_follows_focus;
if let Some(monitor) = wm.focused_monitor_mut() {
monitor.focus_workspace(notification.workspace_idx)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
}
// Drop our lock on the window manager state here to not slow down updates
drop(wm);
// Check if there was an alt-tab across workspaces in the last second
if let Some(hwnd) = ALT_TAB_HWND.load() {
if ALT_TAB_HWND_INSTANT
.lock()
.elapsed()
.lt(&Duration::from_secs(1))
{
// Sleep for 100 millis to let other events pass
std::thread::sleep(Duration::from_millis(100));
tracing::info!("focusing alt-tabbed window");
// Take a new lock on the wm and try to focus the container with
// the recorded HWND from the alt-tab
let mut wm = arc.lock();
if let Ok(workspace) = wm.focused_workspace_mut() {
// Regardless of if this fails, we need to get past this part
// to unblock the border manager below
let _ = workspace.focus_container_by_window(hwnd);
}
// Unblock the border manager
ALT_TAB_HWND.store(None);
// Send a notification to the border manager to update the borders
border_manager::send_notification();
}
}
}
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.28-dev.0"
version = "0.1.25-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,4 +1,4 @@
#Requires AutoHotkey v2.0.2
; Generated by komorebic.exe
Start(ffm, await_configuration, tcp_port) {
RunWait("komorebic.exe start " ffm " --await-configuration " await_configuration " --tcp-port " tcp_port, , "Hide")

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.28-dev.0"
version = "0.1.25-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,15 +11,15 @@ 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" }
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono = "0.4"
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"
@@ -28,14 +28,11 @@ reqwest = { version = "0.12", features = ["blocking"] }
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = "0.9"
sysinfo = { workspace = true }
sysinfo = "0.30"
thiserror = "1"
uds_windows = "1"
which = "6"
win32-display-data = { workspace = true }
windows = { workspace = true }
shadow-rs = { workspace = true }
[build-dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
shadow-rs = { workspace = true }

View File

@@ -5,6 +5,4 @@ fn main() {
).unwrap().text().unwrap();
std::fs::write("applications.yaml", applications_yaml).unwrap();
}
shadow_rs::new().unwrap();
}

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