Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z
41e7b4a9a0 feat(config): add static json loader and whkd flag
This commit is an implementation of a static JSON configuration loader.

An example komorebi.json configuration file has been added.

The application-specific configurations can be loaded directly from a
file, and workspace configuration can be defined declaratively in the
JSON. Individual rules etc. can also be added directly in the static
configuration as one-offs.

A JSONSchema can be generated using komorebic's static-config-schema
command. This should be added to something like SchemaStore later.

Loading from static configuration is significantly faster on startup, as
the lock does not have to be reacquired for every command that is sent
over the socket.

When loading configuration from a static JSON file, a hotwatch instance
will automatically be created to listen to file changes and apply any
updates to both the global and window manager configuration state.

A new --whkd flag has been added to the komorebic start command to
optionally start whkd in a background process.

A new komorebic command 'generate-static-config' has been added to help
existing users migrate to a static JSON config file. Currently, custom
layout file path information can not be automatically populated in the
output of this command and must be added manually by the user if
required.

A new komorebic command 'fetch-asc' has been added to help users update
to the latest version of the application-specific configurations
in-place.

re #427
2023-07-13 08:11:21 -07:00
41 changed files with 1263 additions and 3351 deletions

View File

@@ -28,7 +28,7 @@ jobs:
target:
- x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Prep cargo dirs
@@ -104,7 +104,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v')
with:
version: latest
args: release --skip-validate --clean --release-notes=CHANGELOG.md
args: release --skip-validate --rm-dist --release-notes=CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
@@ -116,7 +116,7 @@ jobs:
winget:
name: Publish to WinGet
runs-on: ubuntu-latest
runs-on: windows-latest
needs: build
if: startsWith(github.ref, 'refs/tags/v')

View File

@@ -26,19 +26,13 @@ builds:
post:
- mkdir -p dist/windows_amd64
- 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"]
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"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
- replacements:
windows: pc-windows-msvc
amd64: x86_64
format: zip
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Arch }}-{{ .Os }}"
files:
- LICENSE
- CHANGELOG.md

908
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,18 @@
[workspace]
resolver = "2"
members = [
"derive-ahk",
"komorebi",
"komorebi-core",
"komorebic",
"komorebic-no-console",
]
[workspace.dependencies]
windows-interface = { version = "0.52" }
windows-implement = { version = "0.52" }
dunce = "1"
dirs = "5"
color-eyre = "0.6"
windows-interface = { version = "0.48" }
windows-implement = { version = "0.48" }
[workspace.dependencies.windows]
version = "0.52"
version = "0.48"
features = [
"implement",
"Win32_System_Com",

View File

@@ -13,14 +13,14 @@ Tiling Window Management for Windows.
<a href="https://github.com/sponsors/LGUG2Z">
<img alt="GitHub Sponsors" src="https://img.shields.io/github/sponsors/LGUG2Z">
</a>
<a href="https://ko-fi.com/lgug2z">
<img alt="Ko-fi" src="https://img.shields.io/badge/kofi-tip-green">
</a>
<a href="https://notado.app/feeds/jado/software-development">
<img alt="Notado Feed" src="https://img.shields.io/badge/Notado-Subscribe-informational">
</a>
<a href="https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg?sub_confirmation=1">
<img alt="YouTube" src="https://img.shields.io/youtube/channel/subscribers/UCeai3-do-9O4MNy9_xjO6mg">
<a href="https://jeezy.substack.com">
<img alt="Substack Read" src="https://img.shields.io/badge/Substack-Read-orange">
</a>
<a href="https://twitter.com/LGUG2Z">
<img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/LGUG2Z">
</a>
</p>
@@ -88,20 +88,21 @@ Articles, blog posts, demos, and videos about _komorebi_ can be added to this li
_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.
I encourage you to make a charitable donation to the [Palestine Children's
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) and [Fresh
Start Refugee](https://www.freshstartrefugee.org/donate) before you consider
sponsoring me on GitHub.
I encourage you to make a charitable donation
to [Fresh Start Refugee](https://www.freshstartrefugee.org/donate) before
you consider sponsoring me on GitHub.
## Project Sponsorship
## GitHub Sponsors
[GitHub Sponsors is enabled for this
project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have
anything specific to offer besides my gratitude and shout outs at the end of
_komorebi_ live development videos and tutorials.
[GitHub Sponsors is enabled for this project](https://github.com/sponsors/LGUG2Z). Users who sponsor my work
on `komorebi` at any of the predefined monthly tiers will be given access to a private fork of this repository where I
push features-in-progress that are not yet quite ready to be pushed on the main repository.
If you would like to tip or sponsor the project but are unable to use GitHub
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z).
There will never be any feature of `komorebi` that is gated behind sponsorship; every new feature will always be
available for free in the public repository once it meets the requisite level of code quality and completion.
Features-in-progress that are available in early access will be tagged in the issues with
an ["early access" label](https://github.com/LGUG2Z/komorebi/issues?q=is%3Aopen+is%3Aissue+label%3A%22early+access%22).
## Demonstrations
@@ -159,13 +160,6 @@ This means that:
### Quickstart
It highly recommended that you enable support for long paths in Windows by running the following command in an
Administrator Terminal before starting with this quickstart guide:
```powershell
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
```
Make sure that you have either the [Scoop Package Manager](https://scoop.sh) or WinGet installed, then run the following
commands at a PowerShell prompt. If you are using WinGet, make sure that you open a new terminal window or reload your
profile after running the installation steps. Since this is not required when using `scoop`, I personally recommend that
@@ -186,27 +180,27 @@ winget install LGUG2Z.whkd
winget install LGUG2Z.komorebi
# save the example configuration to ~/komorebi.json
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/komorebi.example.json -OutFile "$Env:USERPROFILE\komorebi.json"
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.example.json -OutFile $Env:USERPROFILE\komorebi.example.json
# save the latest generated app-specific config tweaks and fixes
komorebic fetch-app-specific-configuration
# ensure the ~/.config folder exists
mkdir "$Env:USERPROFILE\.config" -ea 0
mkdir $Env:USERPROFILE\.config -ea 0
# save the sample whkdrc file with key bindings to ~/.config/whkdrc
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/whkdrc.sample -OutFile "$Env:USERPROFILE\.config\whkdrc"
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/whkdrc.sample -OutFile $Env:USERPROFILE\.config\whkdrc
# start komorebi and whkd
komorebic start -c "$Env:USERPROFILE\komorebi.json" --whkd
komorebic start -c $Env:USERPROFILE\komorebi.json --whkd
```
Thanks to [@sitiom](https://github.com/sitiom) for getting _komorebi_ added to both the popular Scoop Extras bucket and
to WinGet.
You can watch a walkthrough video of this quickstart below on YouTube.
<!-- You can watch a walkthrough video of this quickstart below on YouTube. -->
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/hDDxtvpjpHs/hqdefault.jpg)](https://www.youtube.com/watch?v=hDDxtvpjpHs)
<!-- [![Watch the quickstart walkthrough video](https://img.youtube.com/vi/cBnLIwMtv8g/hqdefault.jpg)](https://www.youtube.com/watch?v=cBnLIwMtv8g) -->
#### Using Autohotkey
@@ -222,13 +216,13 @@ it solely to handle hotkey bindings.
```powershell
# save the latest generated komorebic library to ~/komorebic.lib.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.19/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/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.19/komorebi.generated.ahk -OutFile $Env:USERPROFILE\komorebi.generated.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/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.19/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/master/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
```
### GitHub Releases
@@ -278,7 +272,7 @@ of the window manager. There is a [complete JSON Schema for this configuration f
users with auto-completions in their editors.
If you are running with a dynamic configuration script as recommended in v0.1.16 and earlier, `komorebi` will find the
sample `komorebi.ps1` file in your `$Env:USERPROFILE` directory and automatically load it. This file also starts `whkd` using the sample `whkdrc` file
sample `komorebi.ps1` file in your `$Env:USERPROFILE` directory and automatically load it. This file also starts `whkd` using the sample `whkrc` file
in your `$Env:USERPROFILE\.config` directory.
Alternatively, if you have AutoHotKey installed and a `komorebi.ahk` file in `$Env:UserProfile` directory, `komorebi`
@@ -299,8 +293,6 @@ There are four configuration options that you may need to set yourself, if you m
- The applications.yaml path
- Any individual application rules you have that are not in applications.yaml
[![Watch the tutorial video](https://img.youtube.com/vi/yqCAOJgL3C0/hqdefault.jpg)](https://www.youtube.com/watch?v=yqCAOJgL3C0)
#### Configuration with `komorebic`
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I

View File

@@ -13,23 +13,24 @@ fmt:
prettier --write .github/FUNDING.yml
prettier --write .github/workflows/windows.yaml
install-target target:
cargo +stable install --path {{ target }} --locked
install-komorebic:
cargo +stable install --path komorebic --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-komorebi:
cargo +stable install --path komorebi --locked
install:
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi
just install-komorebic
just install-komorebi
komorebic ahk-asc '~/komorebi-application-specific-configuration/applications.yaml'
komorebic pwsh-asc '~/komorebi-application-specific-configuration/applications.yaml'
cat '~/.config/komorebi/komorebi.generated.ps1' >komorebi.generated.ps1
cat '~/.config/komorebi/komorebi.generated.ahk' >komorebi.generated.ahk
cat '~/.config/komorebi/komorebic.lib_newV2.ahk' >komorebic.lib.ahk
run:
just install-komorebic
cargo +stable run --bin komorebi --locked
cargo +stable run --bin komorebi --locked -- -a
warn $RUST_LOG="warn":
just run

View File

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

View File

@@ -1,43 +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,
)]
#[strum(serialize_all = "snake_case")]
pub enum EaseEnum {
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

@@ -130,7 +130,85 @@ impl Arrangement for DefaultLayout {
layouts
}
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
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 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,
));
}
}
}
layouts
}
};
dimensions
@@ -508,190 +586,3 @@ fn recursive_fibonacci(
res
}
}
fn calculate_ultrawide_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 => (),
2 => {
let (primary, secondary) = result.split_at_mut(1);
let primary = &mut primary[0];
let secondary = &mut secondary[0];
// With two containers on screen container 0 is on the right
if let Some(resize_primary) = resize_dimensions[0] {
resize_left(primary, resize_primary.left);
resize_right(secondary, resize_primary.left);
}
if let Some(resize_secondary) = resize_dimensions[1] {
resize_left(primary, resize_secondary.right);
resize_right(secondary, resize_secondary.right);
}
}
_ => {
let (primary, rest) = result.split_at_mut(1);
let (secondary, tertiary) = rest.split_at_mut(1);
let primary = &mut primary[0];
let secondary = &mut secondary[0];
// With three or more containers container 0 is in the center
if let Some(resize_primary) = resize_dimensions[0] {
resize_left(primary, resize_primary.left);
resize_right(primary, resize_primary.right);
resize_right(secondary, resize_primary.left);
for vertical_element in &mut *tertiary {
resize_left(vertical_element, resize_primary.right);
}
}
// Container 1 is on the left
if let Some(resize_secondary) = resize_dimensions[1] {
resize_left(primary, resize_secondary.right);
resize_right(secondary, resize_secondary.right);
}
// Handle stack on the right
for (i, rect) in resize_dimensions[2..].iter().enumerate() {
if let Some(rect) = rect {
resize_right(primary, rect.left);
tertiary
.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 tertiary[i - 1], rect.top);
resize_top(&mut tertiary[i], rect.top);
}
// Containers in stack except last can be resized down displacing container
// below them
if i != tertiary.len() - 1 {
resize_bottom(&mut tertiary[i], rect.bottom);
resize_top(&mut tertiary[i + 1], rect.bottom);
}
}
}
}
};
result
}
fn resize_left(rect: &mut Rect, resize: i32) {
rect.left += resize / 2;
rect.right += -resize / 2;
}
fn resize_right(rect: &mut Rect, resize: i32) {
rect.right += resize / 2;
}
fn resize_top(rect: &mut Rect, resize: i32) {
rect.top += resize / 2;
rect.bottom += -resize / 2;
}
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

@@ -50,22 +50,10 @@ impl ApplicationOptions {
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifier {
pub kind: ApplicationIdentifier,
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub matching_strategy: Option<MatchingStrategy>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum MatchingStrategy {
Legacy,
Equals,
StartsWith,
EndsWith,
Contains,
Regex,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -74,18 +62,6 @@ pub struct IdWithIdentifierAndComment {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub matching_strategy: Option<MatchingStrategy>,
}
impl From<IdWithIdentifierAndComment> for IdWithIdentifier {
fn from(value: IdWithIdentifierAndComment) -> Self {
Self {
kind: value.kind,
id: value.id.clone(),
matching_strategy: value.matching_strategy,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -98,19 +74,6 @@ pub struct ApplicationConfiguration {
pub float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
}
impl ApplicationConfiguration {
pub fn populate_default_matching_strategies(&mut self) {
if self.identifier.matching_strategy.is_none() {
match self.identifier.kind {
ApplicationIdentifier::Exe => {
self.identifier.matching_strategy = Option::from(MatchingStrategy::Equals);
}
ApplicationIdentifier::Class | ApplicationIdentifier::Title => {}
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ApplicationConfigurationGenerator;
@@ -121,10 +84,6 @@ impl ApplicationConfigurationGenerator {
pub fn format(content: &str) -> Result<String> {
let mut cfgen = Self::load(content)?;
for cfg in &mut cfgen {
cfg.populate_default_matching_strategies();
}
cfgen.sort_by(|a, b| a.name.cmp(&b.name));
Ok(serde_yaml::to_string(&cfgen)?)
}

View File

@@ -3,10 +3,9 @@ use std::fs::File;
use std::io::BufReader;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::Path;
use std::path::PathBuf;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -32,20 +31,23 @@ impl DerefMut for CustomLayout {
}
impl CustomLayout {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
pub fn from_path_buf(path: PathBuf) -> Result<Self> {
let invalid_filetype = anyhow!("custom layouts must be json or yaml files");
let layout: Self = match path.extension() {
Some(extension) if extension == "yaml" || extension == "yml" => {
serde_json::from_reader(BufReader::new(File::open(path)?))?
Some(extension) => {
if extension == "yaml" || extension == "yml" {
serde_yaml::from_reader(BufReader::new(File::open(path)?))?
} else if extension == "json" {
serde_json::from_reader(BufReader::new(File::open(path)?))?
} else {
return Err(invalid_filetype);
}
}
Some(extension) if extension == "json" => {
serde_json::from_reader(BufReader::new(File::open(path)?))?
}
_ => return Err(anyhow!("custom layouts must be json or yaml files")),
None => return Err(invalid_filetype),
};
if !layout.is_valid() {
bail!("the layout file provided was invalid");
return Err(anyhow!("the layout file provided was invalid"));
}
Ok(layout)

View File

@@ -20,7 +20,6 @@ pub enum DefaultLayout {
VerticalStack,
HorizontalStack,
UltrawideVerticalStack,
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
}
impl DefaultLayout {
@@ -34,7 +33,7 @@ impl DefaultLayout {
sizing: Sizing,
delta: i32,
) -> Option<Rect> {
if !matches!(self, Self::BSP) && !matches!(self, Self::UltrawideVerticalStack) {
if !matches!(self, Self::BSP) {
return None;
};
@@ -126,28 +125,4 @@ impl DefaultLayout {
Option::from(r)
}
}
#[must_use]
pub const fn cycle_next(self) -> Self {
match self {
Self::BSP => Self::Columns,
Self::Columns => Self::Rows,
Self::Rows => Self::VerticalStack,
Self::VerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::BSP,
}
}
#[must_use]
pub const fn cycle_previous(self) -> Self {
match self {
Self::BSP => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::VerticalStack,
Self::VerticalStack => Self::Rows,
Self::Rows => Self::Columns,
Self::Columns => Self::BSP,
}
}
}

View File

@@ -1,12 +1,10 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::use_self)]
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use clap::ValueEnum;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -14,7 +12,6 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
pub use animation::EaseEnum;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
pub use custom_layout::CustomLayout;
@@ -25,7 +22,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;
@@ -81,7 +77,6 @@ pub enum SocketMessage {
AdjustContainerPadding(Sizing, i32),
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(DefaultLayout),
CycleLayout(CycleDirection),
ChangeLayoutCustom(PathBuf),
FlipLayout(Axis),
// Monitor and Workspace Commands
@@ -106,10 +101,8 @@ pub enum SocketMessage {
FocusNamedWorkspace(String),
ContainerPadding(usize, usize, i32),
NamedWorkspaceContainerPadding(String, i32),
FocusedWorkspaceContainerPadding(i32),
WorkspacePadding(usize, usize, i32),
NamedWorkspacePadding(String, i32),
FocusedWorkspacePadding(i32),
WorkspaceTiling(usize, usize, bool),
NamedWorkspaceTiling(String, bool),
WorkspaceName(usize, usize, String),
@@ -129,9 +122,6 @@ pub enum SocketMessage {
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
Animation(bool),
AnimationDuration(u64),
AnimationEase(EaseEnum),
ActiveWindowBorder(bool),
ActiveWindowBorderColour(WindowKind, u32, u32, u32),
ActiveWindowBorderWidth(i32),
@@ -160,7 +150,6 @@ pub enum SocketMessage {
ToggleTitleBars,
AddSubscriber(String),
RemoveSubscriber(String),
ApplicationSpecificConfigurationSchema,
NotificationSchema,
SocketSchema,
StaticConfigSchema,
@@ -203,17 +192,7 @@ pub enum StateQuery {
}
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum ApplicationIdentifier {
@@ -306,36 +285,3 @@ impl Sizing {
}
}
}
pub fn resolve_home_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let mut resolved_path = PathBuf::new();
let mut resolved = false;
for c in path.as_ref().components() {
match c {
std::path::Component::Normal(c)
if (c == "~" || c == "$Env:USERPROFILE" || c == "$HOME") && !resolved =>
{
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
resolved_path.extend(home.components());
resolved = true;
}
_ => resolved_path.push(c),
}
}
let parent = resolved_path
.parent()
.ok_or_else(|| anyhow!("cannot parse parent directory"))?;
Ok(if parent.is_dir() {
let file = resolved_path
.components()
.last()
.ok_or_else(|| anyhow!("cannot parse filename"))?;
dunce::canonicalize(parent)?.join(file)
} else {
resolved_path
})
}

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/master/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

View File

@@ -27,10 +27,6 @@ RunWait('komorebic.exe identify-tray-application exe "Akiflow.exe"', , "Hide")
; Android Studio
RunWait('komorebic.exe identify-object-name-change-application exe "studio64.exe"', , "Hide")
; Anki
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "anki.exe"', , "Hide")
; ArmCord
RunWait('komorebic.exe identify-border-overflow-application exe "ArmCord.exe"', , "Hide")
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
@@ -40,7 +36,6 @@ RunWait('komorebic.exe identify-tray-application exe "ArmCord.exe"', , "Hide")
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "AutoHotkeyU64.exe"', , "Hide")
RunWait('komorebic.exe float-rule title "Window Spy"', , "Hide")
RunWait('komorebic.exe float-rule exe "AutoHotkeyUX.exe"', , "Hide")
; Beeper
RunWait('komorebic.exe identify-border-overflow-application exe "Beeper.exe"', , "Hide")
@@ -57,19 +52,6 @@ RunWait('komorebic.exe float-rule exe "Bloxstrap.exe"', , "Hide")
; Calculator
RunWait('komorebic.exe float-rule title "Calculator"', , "Hide")
; Clash Verge
RunWait('komorebic.exe identify-border-overflow-application exe "Clash Verge.exe"', , "Hide")
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "Clash Verge.exe"', , "Hide")
; Clementine
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "clementine.exe"', , "Hide")
; CopyQ
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "copyq.exe"', , "Hide")
; Credential Manager UI Host
; Targets the Windows popup prompting you for a PIN instead of a password on 1Password etc.
RunWait('komorebic.exe float-rule exe "CredentialUIBroker.exe"', , "Hide")
@@ -109,9 +91,6 @@ RunWait('komorebic.exe identify-border-overflow-application exe "DiscordPTB.exe"
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "DiscordPTB.exe"', , "Hide")
; Docker Desktop
RunWait('komorebic.exe identify-border-overflow-application exe "Docker Desktop.exe"', , "Hide")
; Dropbox
RunWait('komorebic.exe float-rule exe "Dropbox.exe"', , "Hide")
@@ -144,13 +123,6 @@ RunWait('komorebic.exe identify-border-overflow-application exe "EpicGamesLaunch
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "EpicGamesLauncher.exe"', , "Hide")
; Everything
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "Everything.exe"', , "Hide")
; Figma
RunWait('komorebic.exe identify-border-overflow-application exe "Figma.exe"', , "Hide")
; Flow Launcher
RunWait('komorebic.exe identify-border-overflow-application exe "Flow.Launcher.exe"', , "Hide")
@@ -171,17 +143,11 @@ RunWait('komorebic.exe identify-border-overflow-application exe "GodotManager.ex
RunWait('komorebic.exe manage-rule exe "GodotManager.exe"', , "Hide")
RunWait('komorebic.exe identify-object-name-change-application exe "GodotManager.exe"', , "Hide")
; Golden Dict
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "GoldenDict.exe"', , "Hide")
; Google Chrome
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "chrome.exe"', , "Hide")
; Google Drive
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "GoogleDriveFS.exe"', , "Hide")
RunWait('komorebic.exe float-rule exe "GoogleDriveFS.exe"', , "Hide")
; Houdoku
@@ -290,10 +256,6 @@ RunWait('komorebic.exe identify-border-overflow-application exe "NVIDIA GeForce
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "NZXT CAM.exe"', , "Hide")
; NetEase Cloud Music
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "cloudmusic.exe"', , "Hide")
; NiceHash Miner
RunWait('komorebic.exe identify-border-overflow-application exe "nhm_app.exe"', , "Hide")
RunWait('komorebic.exe manage-rule exe "nhm_app.exe"', , "Hide")
@@ -326,10 +288,6 @@ RunWait('komorebic.exe manage-rule exe "Obsidian.exe"', , "Hide")
; OneDrive
RunWait('komorebic.exe float-rule class "OneDriveReactNativeWin32WindowClass"', , "Hide")
; OneQuick
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "OneQuick.exe"', , "Hide")
; OpenRGB
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "OpenRGB.exe"', , "Hide")
@@ -337,13 +295,6 @@ RunWait('komorebic.exe identify-tray-application exe "OpenRGB.exe"', , "Hide")
; Paradox Launcher
RunWait('komorebic.exe float-rule exe "Paradox Launcher.exe"', , "Hide")
; Playnite
RunWait('komorebic.exe identify-border-overflow-application exe "Playnite.DesktopApp.exe"', , "Hide")
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "Playnite.DesktopApp.exe"', , "Hide")
; Target fullscreen app
RunWait('komorebic.exe float-rule exe "Playnite.FullscreenApp.exe"', , "Hide")
; Plexamp
RunWait('komorebic.exe identify-border-overflow-application exe "Plexamp.exe"', , "Hide")
@@ -370,13 +321,6 @@ RunWait('komorebic.exe identify-object-name-change-application exe "pycharm64.ex
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "pycharm64.exe"', , "Hide")
; QQ
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "QQ.exe"', , "Hide")
RunWait('komorebic.exe float-rule title "图片查看器"', , "Hide")
RunWait('komorebic.exe float-rule title "群聊的聊天记录"', , "Hide")
RunWait('komorebic.exe float-rule title "语音通话"', , "Hide")
; QtScrcpy
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "QtScrcpy.exe"', , "Hide")
@@ -404,15 +348,6 @@ RunWait('komorebic.exe identify-border-overflow-application exe "RoundedTB.exe"'
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "RoundedTB.exe"', , "Hide")
; RustRover
RunWait('komorebic.exe identify-object-name-change-application exe "rustrover64.exe"', , "Hide")
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "rustrover64.exe"', , "Hide")
; Sandboxie Plus
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "SandMan.exe"', , "Hide")
; ShareX
RunWait('komorebic.exe identify-border-overflow-application exe "ShareX.exe"', , "Hide")
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
@@ -423,7 +358,7 @@ RunWait('komorebic.exe float-rule exe "sideloadly.exe"', , "Hide")
; Signal
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "Signal.exe"', , "Hide")
RunWait('komorebic.exe identify-tray-application exe "signal.exe"', , "Hide")
; SiriKali
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
@@ -549,10 +484,6 @@ RunWait('komorebic.exe float-rule title "Control Panel"', , "Hide")
; Windows Installer
RunWait('komorebic.exe float-rule exe "msiexec.exe"', , "Hide")
; Windows Subsystem for Android
; Targets splash/startup screen
RunWait('komorebic.exe float-rule class "android(splash)"', , "Hide")
; WingetUI
; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
RunWait('komorebic.exe identify-tray-application exe "WingetUI.exe"', , "Hide")
@@ -572,9 +503,6 @@ RunWait('komorebic.exe identify-tray-application exe "xampp-control.exe"', , "Hi
; Zoom
RunWait('komorebic.exe float-rule exe "Zoom.exe"', , "Hide")
; mpv
RunWait('komorebic.exe identify-object-name-change-application class "mpv"', , "Hide")
; mpv.net
RunWait('komorebic.exe identify-object-name-change-application exe "mpvnet.exe"', , "Hide")

View File

@@ -27,10 +27,6 @@ komorebic.exe identify-tray-application exe "Akiflow.exe"
# Android Studio
komorebic.exe identify-object-name-change-application exe "studio64.exe"
# Anki
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "anki.exe"
# ArmCord
komorebic.exe identify-border-overflow-application exe "ArmCord.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
@@ -40,7 +36,6 @@ komorebic.exe identify-tray-application exe "ArmCord.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "AutoHotkeyU64.exe"
komorebic.exe float-rule title "Window Spy"
komorebic.exe float-rule exe "AutoHotkeyUX.exe"
# Beeper
komorebic.exe identify-border-overflow-application exe "Beeper.exe"
@@ -57,19 +52,6 @@ komorebic.exe float-rule exe "Bloxstrap.exe"
# Calculator
komorebic.exe float-rule title "Calculator"
# Clash Verge
komorebic.exe identify-border-overflow-application exe "Clash Verge.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "Clash Verge.exe"
# Clementine
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "clementine.exe"
# CopyQ
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "copyq.exe"
# Credential Manager UI Host
# Targets the Windows popup prompting you for a PIN instead of a password on 1Password etc.
komorebic.exe float-rule exe "CredentialUIBroker.exe"
@@ -109,9 +91,6 @@ komorebic.exe identify-border-overflow-application exe "DiscordPTB.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "DiscordPTB.exe"
# Docker Desktop
komorebic.exe identify-border-overflow-application exe "Docker Desktop.exe"
# Dropbox
komorebic.exe float-rule exe "Dropbox.exe"
@@ -144,13 +123,6 @@ komorebic.exe identify-border-overflow-application exe "EpicGamesLauncher.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "EpicGamesLauncher.exe"
# Everything
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "Everything.exe"
# Figma
komorebic.exe identify-border-overflow-application exe "Figma.exe"
# Flow Launcher
komorebic.exe identify-border-overflow-application exe "Flow.Launcher.exe"
@@ -171,17 +143,11 @@ komorebic.exe identify-border-overflow-application exe "GodotManager.exe"
komorebic.exe manage-rule exe "GodotManager.exe"
komorebic.exe identify-object-name-change-application exe "GodotManager.exe"
# Golden Dict
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "GoldenDict.exe"
# Google Chrome
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "chrome.exe"
# Google Drive
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "GoogleDriveFS.exe"
komorebic.exe float-rule exe "GoogleDriveFS.exe"
# Houdoku
@@ -290,10 +256,6 @@ komorebic.exe identify-border-overflow-application exe "NVIDIA GeForce Experienc
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "NZXT CAM.exe"
# NetEase Cloud Music
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "cloudmusic.exe"
# NiceHash Miner
komorebic.exe identify-border-overflow-application exe "nhm_app.exe"
komorebic.exe manage-rule exe "nhm_app.exe"
@@ -326,10 +288,6 @@ komorebic.exe manage-rule exe "Obsidian.exe"
# OneDrive
komorebic.exe float-rule class "OneDriveReactNativeWin32WindowClass"
# OneQuick
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "OneQuick.exe"
# OpenRGB
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "OpenRGB.exe"
@@ -337,13 +295,6 @@ komorebic.exe identify-tray-application exe "OpenRGB.exe"
# Paradox Launcher
komorebic.exe float-rule exe "Paradox Launcher.exe"
# Playnite
komorebic.exe identify-border-overflow-application exe "Playnite.DesktopApp.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "Playnite.DesktopApp.exe"
# Target fullscreen app
komorebic.exe float-rule exe "Playnite.FullscreenApp.exe"
# Plexamp
komorebic.exe identify-border-overflow-application exe "Plexamp.exe"
@@ -370,13 +321,6 @@ komorebic.exe identify-object-name-change-application exe "pycharm64.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "pycharm64.exe"
# QQ
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "QQ.exe"
komorebic.exe float-rule title "图片查看器"
komorebic.exe float-rule title "群聊的聊天记录"
komorebic.exe float-rule title "语音通话"
# QtScrcpy
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "QtScrcpy.exe"
@@ -404,15 +348,6 @@ komorebic.exe identify-border-overflow-application exe "RoundedTB.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "RoundedTB.exe"
# RustRover
komorebic.exe identify-object-name-change-application exe "rustrover64.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "rustrover64.exe"
# Sandboxie Plus
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "SandMan.exe"
# ShareX
komorebic.exe identify-border-overflow-application exe "ShareX.exe"
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
@@ -423,7 +358,7 @@ komorebic.exe float-rule exe "sideloadly.exe"
# Signal
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "Signal.exe"
komorebic.exe identify-tray-application exe "signal.exe"
# SiriKali
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
@@ -549,10 +484,6 @@ komorebic.exe float-rule title "Control Panel"
# Windows Installer
komorebic.exe float-rule exe "msiexec.exe"
# Windows Subsystem for Android
# Targets splash/startup screen
komorebic.exe float-rule class "android(splash)"
# WingetUI
# If you have disabled minimize/close to tray for this application, you can delete/comment out the next line
komorebic.exe identify-tray-application exe "WingetUI.exe"
@@ -572,9 +503,6 @@ komorebic.exe identify-tray-application exe "xampp-control.exe"
# Zoom
komorebic.exe float-rule exe "Zoom.exe"
# mpv
komorebic.exe identify-object-name-change-application class "mpv"
# mpv.net
komorebic.exe identify-object-name-change-application exe "mpvnet.exe"

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.19"
version = "0.1.16"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -15,9 +15,11 @@ komorebi-core = { path = "../komorebi-core" }
bitflags = "2"
clap = { version = "4", features = ["derive"] }
color-eyre = "0.6"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
ctrlc = "3"
dirs = "5"
getset = "0.1"
hotwatch = "0.4"
lazy_static = "1"
@@ -27,7 +29,6 @@ net2 = "0.2"
os_info = "3.7"
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
paste = "1"
regex = "1"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -37,14 +38,12 @@ tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uds_windows = "1"
which = "5"
which = "4"
winput = "0.2"
winreg = "0.52"
winreg = "0.50"
windows-interface = { workspace = true }
windows-implement = { workspace = true }
windows = { workspace = true }
color-eyre = { workspace = true }
dirs = { workspace = true }
[features]
deadlock_detection = []

View File

@@ -1,510 +0,0 @@
use color_eyre::Result;
use komorebi_core::EaseEnum;
use komorebi_core::Rect;
use schemars::JsonSchema;
use std::f64::consts::PI;
use std::time::Duration;
use std::time::Instant;
use crate::ANIMATION_EASE;
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 ease = *ANIMATION_EASE.lock();
match ease {
EaseEnum::Linear => Linear::evaluate(t),
EaseEnum::EaseInSine => EaseInSine::evaluate(t),
EaseEnum::EaseOutSine => EaseOutSine::evaluate(t),
EaseEnum::EaseInOutSine => EaseInOutSine::evaluate(t),
EaseEnum::EaseInQuad => EaseInQuad::evaluate(t),
EaseEnum::EaseOutQuad => EaseOutQuad::evaluate(t),
EaseEnum::EaseInOutQuad => EaseInOutQuad::evaluate(t),
EaseEnum::EaseInCubic => EaseInCubic::evaluate(t),
EaseEnum::EaseInOutCubic => EaseInOutCubic::evaluate(t),
EaseEnum::EaseInQuart => EaseInQuart::evaluate(t),
EaseEnum::EaseOutQuart => EaseOutQuart::evaluate(t),
EaseEnum::EaseInOutQuart => EaseInOutQuart::evaluate(t),
EaseEnum::EaseInQuint => EaseInQuint::evaluate(t),
EaseEnum::EaseOutQuint => EaseOutQuint::evaluate(t),
EaseEnum::EaseInOutQuint => EaseInOutQuint::evaluate(t),
EaseEnum::EaseInExpo => EaseInExpo::evaluate(t),
EaseEnum::EaseOutExpo => EaseOutExpo::evaluate(t),
EaseEnum::EaseInOutExpo => EaseInOutExpo::evaluate(t),
EaseEnum::EaseInCirc => EaseInCirc::evaluate(t),
EaseEnum::EaseOutCirc => EaseOutCirc::evaluate(t),
EaseEnum::EaseInOutCirc => EaseInOutCirc::evaluate(t),
EaseEnum::EaseInBack => EaseInBack::evaluate(t),
EaseEnum::EaseOutBack => EaseOutBack::evaluate(t),
EaseEnum::EaseInOutBack => EaseInOutBack::evaluate(t),
EaseEnum::EaseInElastic => EaseInElastic::evaluate(t),
EaseEnum::EaseOutElastic => EaseOutElastic::evaluate(t),
EaseEnum::EaseInOutElastic => EaseInOutElastic::evaluate(t),
EaseEnum::EaseInBounce => EaseInBounce::evaluate(t),
EaseEnum::EaseOutBounce => EaseOutBounce::evaluate(t),
EaseEnum::EaseInOutBounce => EaseInOutBounce::evaluate(t),
}
}
#[derive(Debug, Default, Clone, Copy, JsonSchema)]
pub struct Animation {
// is_cancel: AtomicBool,
// pub in_progress: AtomicBool,
is_cancel: bool,
pub in_progress: bool,
}
// impl Default for Animation {
// fn default() -> Self {
// Animation {
// // I'm not sure if this is the right way to do it
// // I've tried to use Arc<Mutex<bool>> but it dooes not implement Copy trait
// // and I dont want to rewrite everything cause I'm not experienced with rust
// // Down here you can see the idea I've tried to achive like in any other OOP language
// // My thought is that in order to prevent Google Chrome breaking render window
// // I need to cancel animation if user starting new window movement. So window stops
// // moving at one point and then fires new animation.
// // But my approach does not work because of rust borrowing rules and wired pointers
// // lifetime annotation that I dont know how to use.
// is_cancel: false,
// in_progress: false,
// // is_cancel: AtomicBool::new(false),
// // in_progress: AtomicBool::new(false),
// }
// }
// }
impl Animation {
pub fn cancel(&mut self) {
if !self.in_progress {
return;
}
self.is_cancel = true;
let max_duration = Duration::from_secs(1);
let spent_duration = Instant::now();
while self.in_progress {
if spent_duration.elapsed() >= max_duration {
self.in_progress = false;
}
std::thread::sleep(Duration::from_millis(16));
}
}
#[allow(clippy::cast_possible_truncation)]
pub fn lerp(x: i32, new_x: i32, t: f64) -> i32 {
let time = apply_ease_func(t);
f64::from(new_x - x).mul_add(time, f64::from(x)) as i32
}
pub fn lerp_rect(original_rect: &Rect, new_rect: &Rect, t: f64) -> Rect {
Rect {
left: Self::lerp(original_rect.left, new_rect.left, t),
top: Self::lerp(original_rect.top, new_rect.top, t),
right: Self::lerp(original_rect.right, new_rect.right, t),
bottom: Self::lerp(original_rect.bottom, new_rect.bottom, t),
}
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
&mut self,
duration: Duration,
mut f: impl FnMut(f64) -> Result<()>,
) -> Result<()> {
self.in_progress = true;
// set target frame time to match 240 fps (my max refresh rate of monitor)
// probably not the best way to do it is take actual monitor refresh rate
// or make it configurable
let target_frame_time = Duration::from_millis(1000 / 240);
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if self.is_cancel {
// cancel animation
// set all flags
self.is_cancel = !self.is_cancel;
self.in_progress = false;
return Ok(());
}
let tick_start = Instant::now();
// calculate progress
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
f(progress).ok();
// sleep until next frame
while tick_start.elapsed() < target_frame_time {
std::thread::sleep(target_frame_time - tick_start.elapsed());
}
}
self.in_progress = false;
// 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
f(progress)
}
}

View File

@@ -14,7 +14,6 @@ use windows::Win32::UI::WindowsAndMessaging::WNDCLASSA;
use komorebi_core::Rect;
use crate::window::should_act;
use crate::window::Window;
use crate::windows_callbacks;
use crate::WindowsApi;
@@ -22,7 +21,6 @@ use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::BORDER_RECT;
use crate::REGEX_IDENTIFIERS;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_11;
@@ -48,7 +46,7 @@ impl Border {
let class_name = PCSTR(name.as_ptr());
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
let window_class = WNDCLASSA {
hInstance: instance.into(),
hInstance: instance,
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(windows_callbacks::border_window),
@@ -110,24 +108,19 @@ impl Border {
Self::create("komorebi-border-window")?;
}
let mut should_expand_border = false;
let mut rect = WindowsApi::window_rect(window.hwnd())?;
rect.top -= invisible_borders.bottom;
rect.bottom += invisible_borders.bottom;
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let title = &window.title()?;
let exe_name = &window.exe()?;
let class = &window.class()?;
let should_expand_border = should_act(
title,
exe_name,
class,
&border_overflows,
&regex_identifiers,
);
if border_overflows.contains(&window.title()?)
|| border_overflows.contains(&window.exe()?)
|| border_overflows.contains(&window.class()?)
{
should_expand_border = true;
}
if should_expand_border {
rect.left -= invisible_borders.left;

View File

@@ -39,7 +39,7 @@ impl Hidden {
let class_name = PCSTR(name.as_ptr());
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
let window_class = WNDCLASSA {
hInstance: instance.into(),
hInstance: instance,
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(windows_callbacks::hidden_window),

View File

@@ -1,10 +1,5 @@
#![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
)]
#![allow(clippy::missing_errors_doc, clippy::redundant_pub_crate)]
use std::collections::HashMap;
use std::fs::File;
@@ -16,24 +11,22 @@ 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;
#[cfg(feature = "deadlock_detection")]
use std::time::Duration;
use clap::Parser;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::Backoff;
use komorebi_core::EaseEnum;
use lazy_static::lazy_static;
use os_info::Version;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
use serde::Serialize;
use sysinfo::Process;
@@ -47,9 +40,6 @@ use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
use crate::hidden::Hidden;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
use komorebi_core::SocketMessage;
@@ -67,7 +57,6 @@ use crate::windows_api::WindowsApi;
#[macro_use]
mod ring;
mod animation;
mod border;
mod com;
mod container;
@@ -92,83 +81,36 @@ type WorkspaceRule = (usize, usize, bool);
lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("steam.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> =
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("explorer.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("chrome.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("idea64.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("ApplicationFrameHost.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("steam.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}
"explorer.exe".to_string(),
"firefox.exe".to_string(),
"chrome.exe".to_string(),
"idea64.exe".to_string(),
"ApplicationFrameHost.exe".to_string(),
"steam.exe".to_string(),
]));
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("idea64.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"firefox.exe".to_string(),
"idea64.exe".to_string(),
]));
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
// mstsc.exe creates these on Windows 11 when a WSL process is launched
// https://github.com/LGUG2Z/komorebi/issues/74
IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: String::from("OPContainerClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: String::from("IHWindowClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}
"OPContainerClass".to_string(),
"IHWindowClass".to_string()
]));
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"Chrome_RenderWidgetHostHWND".to_string(),
]));
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"X410.exe".to_string(),
"vcxsrv.exe".to_string(),
@@ -217,8 +159,6 @@ lazy_static! {
static ref BORDER_OFFSET: Arc<Mutex<Option<Rect>>> =
Arc::new(Mutex::new(None));
static ref ANIMATION_EASE: Arc<Mutex<EaseEnum>> = Arc::new(Mutex::new(EaseEnum::Linear));
// 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![]));
@@ -242,8 +182,6 @@ pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20);
// 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);
@@ -258,7 +196,7 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
std::env::set_var("RUST_LOG", "info");
}
let appender = tracing_appender::rolling::never(&*DATA_DIR, "komorebi.log");
let appender = tracing_appender::rolling::never(DATA_DIR.clone(), "komorebi.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);
@@ -311,8 +249,13 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
}
pub fn load_configuration() -> Result<()> {
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");
let home = HOME_DIR.clone();
let mut config_pwsh = home.clone();
config_pwsh.push("komorebi.ps1");
let mut config_ahk = home;
config_ahk.push("komorebi.ahk");
if config_pwsh.exists() {
let powershell_exe = if which("pwsh.exe").is_ok() {
@@ -321,13 +264,25 @@ pub fn load_configuration() -> Result<()> {
"powershell.exe"
};
tracing::info!("loading configuration file: {}", config_pwsh.display());
tracing::info!(
"loading configuration file: {}",
config_pwsh
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new(powershell_exe)
.arg(config_pwsh.as_os_str())
.output()?;
} else if config_ahk.exists() && which(&*AHK_EXE).is_ok() {
tracing::info!("loading configuration file: {}", config_ahk.display());
tracing::info!(
"loading configuration file: {}",
config_ahk
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new(&*AHK_EXE)
.arg(config_ahk.as_os_str())
@@ -358,7 +313,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
// This is the path on Windows 11
if current.is_none() {
current = hkcu
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
.open_subkey(r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops"#)
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
@@ -397,9 +352,9 @@ pub struct Notification {
pub fn notify_subscribers(notification: &str) -> Result<()> {
let mut stale_subscriptions = vec![];
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
for (subscriber, pipe) in &mut *subscriptions {
for (subscriber, pipe) in subscriptions.iter_mut() {
match writeln!(pipe, "{notification}") {
Ok(()) => {
Ok(_) => {
tracing::debug!("pushed notification to subscriber: {}", subscriber);
}
Err(error) => {
@@ -455,16 +410,16 @@ fn detect_deadlocks() {
#[clap(author, about, version)]
struct Opts {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(short, long = "ffm")]
#[clap(action, short, long = "ffm")]
focus_follows_mouse: bool,
/// Wait for 'komorebic complete-configuration' to be sent before processing events
#[clap(short, long)]
#[clap(action, short, long)]
await_configuration: bool,
/// Start a TCP server on the given port to allow the direct sending of SocketMessages
#[clap(short, long)]
#[clap(action, short, long)]
tcp_port: Option<usize>,
/// Path to a static configuration JSON file
#[clap(short, long)]
#[clap(action, short, long)]
config: Option<PathBuf>,
}
@@ -503,8 +458,6 @@ fn main() -> Result<()> {
// File logging worker guard has to have an assignment in the main fn to work
let (_guard, _color_guard) = setup()?;
WindowsApi::foreground_lock_timeout()?;
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
@@ -516,22 +469,10 @@ fn main() -> Result<()> {
Hidden::create("komorebi-hidden")?;
let static_config = opts.config.map_or_else(
|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
if komorebi_json.is_file() {
Option::from(komorebi_json)
} else {
None
}
},
Option::from,
);
let wm = if let Some(config) = &static_config {
let wm = if let Some(config) = &opts.config {
tracing::info!(
"creating window manager from static configuration file: {}",
config.display()
config.as_os_str().to_str().unwrap()
);
Arc::new(Mutex::new(StaticConfig::preload(
@@ -545,8 +486,7 @@ fn main() -> Result<()> {
};
wm.lock().init()?;
if let Some(config) = &static_config {
if let Some(config) = &opts.config {
StaticConfig::postload(config, &wm)?;
}
@@ -560,8 +500,10 @@ fn main() -> Result<()> {
listen_for_commands_tcp(wm.clone(), port);
}
if static_config.is_none() {
std::thread::spawn(|| load_configuration().expect("could not load configuration"));
if opts.config.is_none() {
std::thread::spawn(|| {
load_configuration().expect("could not load configuration");
});
if opts.await_configuration {
let backoff = Backoff::new();

View File

@@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::collections::VecDeque;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use getset::CopyGetters;
use getset::Getters;
@@ -121,7 +120,9 @@ impl Monitor {
.ok_or_else(|| anyhow!("there is no workspace"))?;
if workspace.maximized_window().is_some() {
bail!("cannot move native maximized window to another monitor or workspace");
return Err(anyhow!(
"cannot move native maximized window to another monitor or workspace"
));
}
let container = workspace

View File

@@ -20,9 +20,6 @@ use parking_lot::Mutex;
use schemars::schema_for;
use uds_windows::UnixStream;
use komorebi_core::config_generation::ApplicationConfiguration;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
use komorebi_core::FocusFollowsMouseImplementation;
@@ -47,9 +44,6 @@ use crate::windows_api::WindowsApi;
use crate::Notification;
use crate::NotificationEvent;
use crate::ALT_FOCUS_HACK;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_EASE;
use crate::ANIMATION_ENABLED;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
@@ -244,40 +238,16 @@ impl WindowManager {
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
}
}
SocketMessage::ManageRule(identifier, ref id) => {
SocketMessage::ManageRule(_, ref id) => {
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
let mut should_push = true;
for m in &*manage_identifiers {
if m.id.eq(id) {
should_push = false;
}
}
if should_push {
manage_identifiers.push(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
if !manage_identifiers.contains(id) {
manage_identifiers.push(id.to_string());
}
}
SocketMessage::FloatRule(identifier, ref id) => {
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut should_push = true;
for f in &*float_identifiers {
if f.id.eq(id) {
should_push = false;
}
}
if should_push {
float_identifiers.push(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
if !float_identifiers.contains(id) {
float_identifiers.push(id.to_string());
}
let invisible_borders = self.invisible_borders;
@@ -289,8 +259,9 @@ impl WindowManager {
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace"))?
.containers()
.iter()
{
for window in container.windows() {
for window in container.windows().iter() {
match identifier {
ApplicationIdentifier::Exe => {
if window.exe()? == *id {
@@ -326,28 +297,6 @@ impl WindowManager {
monitor.update_focused_workspace(offset, &invisible_borders)?;
}
}
SocketMessage::FocusedWorkspaceContainerPadding(adjustment) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_monitor = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
self.set_container_padding(focused_monitor_idx, focused_workspace_idx, adjustment)?;
}
SocketMessage::FocusedWorkspacePadding(adjustment) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_monitor = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
self.set_workspace_padding(focused_monitor_idx, focused_workspace_idx, adjustment)?;
}
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
self.adjust_container_padding(sizing, adjustment)?;
}
@@ -474,12 +423,11 @@ impl WindowManager {
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)?,
SocketMessage::ChangeLayoutCustom(ref path) => {
self.change_workspace_custom_layout(path)?;
self.change_workspace_custom_layout(path.clone())?;
}
SocketMessage::WorkspaceLayoutCustom(monitor_idx, workspace_idx, ref path) => {
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?;
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path.clone())?;
}
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
@@ -510,7 +458,7 @@ impl WindowManager {
monitor_idx,
workspace_idx,
at_container_count,
path,
path.clone(),
)?;
}
SocketMessage::ClearWorkspaceLayoutRules(monitor_idx, workspace_idx) => {
@@ -520,7 +468,7 @@ impl WindowManager {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?;
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path.clone())?;
}
}
SocketMessage::NamedWorkspaceTiling(ref workspace, tile) => {
@@ -561,7 +509,7 @@ impl WindowManager {
monitor_idx,
workspace_idx,
at_container_count,
path,
path.clone(),
)?;
}
}
@@ -695,7 +643,10 @@ impl WindowManager {
Err(error) => error.to_string(),
};
let socket = DATA_DIR.join("komorebic.sock");
let mut socket = DATA_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(socket)?;
stream.write_all(state.as_bytes())?;
}
@@ -715,7 +666,10 @@ impl WindowManager {
}
.to_string();
let socket = DATA_DIR.join("komorebic.sock");
let mut socket = DATA_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(socket)?;
stream.write_all(response.as_bytes())?;
}
@@ -937,75 +891,28 @@ impl WindowManager {
SocketMessage::WatchConfiguration(enable) => {
self.watch_configuration(enable)?;
}
SocketMessage::IdentifyBorderOverflowApplication(identifier, ref id) => {
SocketMessage::IdentifyBorderOverflowApplication(_, ref id) => {
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
}
}
if should_push {
identifiers.push(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
if !identifiers.contains(id) {
identifiers.push(id.to_string());
}
}
SocketMessage::IdentifyObjectNameChangeApplication(identifier, ref id) => {
SocketMessage::IdentifyObjectNameChangeApplication(_, ref id) => {
let mut identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
}
}
if should_push {
identifiers.push(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
if !identifiers.contains(id) {
identifiers.push(id.to_string());
}
}
SocketMessage::IdentifyTrayApplication(identifier, ref id) => {
SocketMessage::IdentifyTrayApplication(_, ref id) => {
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
}
}
if should_push {
identifiers.push(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
if !identifiers.contains(id) {
identifiers.push(id.to_string());
}
}
SocketMessage::IdentifyLayeredApplication(identifier, ref id) => {
SocketMessage::IdentifyLayeredApplication(_, ref id) => {
let mut identifiers = LAYERED_WHITELIST.lock();
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
}
}
if should_push {
identifiers.push(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
if !identifiers.contains(id) {
identifiers.push(id.to_string());
}
}
SocketMessage::ManageFocusedWindow => {
@@ -1032,7 +939,8 @@ impl WindowManager {
let workspace = self.focused_workspace()?;
let resize = workspace.resize_dimensions();
let quicksave_json = std::env::temp_dir().join("komorebi.quicksave.json");
let mut quicksave_json = std::env::temp_dir();
quicksave_json.push("komorebi.quicksave.json");
let file = OpenOptions::new()
.write(true)
@@ -1045,10 +953,15 @@ impl WindowManager {
SocketMessage::QuickLoad => {
let workspace = self.focused_workspace_mut()?;
let quicksave_json = std::env::temp_dir().join("komorebi.quicksave.json");
let mut quicksave_json = std::env::temp_dir();
quicksave_json.push("komorebi.quicksave.json");
let file = File::open(&quicksave_json)
.map_err(|_| anyhow!("no quicksave found at {}", quicksave_json.display()))?;
let file = File::open(&quicksave_json).map_err(|_| {
anyhow!(
"no quicksave found at {}",
quicksave_json.display().to_string()
)
})?;
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
@@ -1063,15 +976,15 @@ impl WindowManager {
.write(true)
.truncate(true)
.create(true)
.open(path)?;
.open(path.clone())?;
serde_json::to_writer_pretty(&file, &resize)?;
}
SocketMessage::Load(ref path) => {
let workspace = self.focused_workspace_mut()?;
let file =
File::open(path).map_err(|_| anyhow!("no file found at {}", path.display()))?;
let file = File::open(path)
.map_err(|_| anyhow!("no file found at {}", path.display().to_string()))?;
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
@@ -1143,15 +1056,6 @@ impl WindowManager {
self.hide_border()?;
}
}
SocketMessage::Animation(enable) => {
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::AnimationDuration(duration) => {
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
}
SocketMessage::AnimationEase(ease) => {
*ANIMATION_EASE.lock() = ease;
}
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => {
match kind {
WindowKind::Single => {
@@ -1189,18 +1093,12 @@ impl WindowManager {
SocketMessage::AltFocusHack(enable) => {
ALT_FOCUS_HACK.store(enable, Ordering::SeqCst);
}
SocketMessage::ApplicationSpecificConfigurationSchema => {
let asc = schema_for!(Vec<ApplicationConfiguration>);
let schema = serde_json::to_string_pretty(&asc)?;
let socket = DATA_DIR.join("komorebic.sock");
let mut stream = UnixStream::connect(socket)?;
stream.write_all(schema.as_bytes())?;
}
SocketMessage::NotificationSchema => {
let notification = schema_for!(Notification);
let schema = serde_json::to_string_pretty(&notification)?;
let socket = DATA_DIR.join("komorebic.sock");
let mut socket = DATA_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(socket)?;
stream.write_all(schema.as_bytes())?;
@@ -1208,7 +1106,9 @@ impl WindowManager {
SocketMessage::SocketSchema => {
let socket_message = schema_for!(SocketMessage);
let schema = serde_json::to_string_pretty(&socket_message)?;
let socket = DATA_DIR.join("komorebic.sock");
let mut socket = DATA_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(socket)?;
stream.write_all(schema.as_bytes())?;
@@ -1216,14 +1116,18 @@ impl WindowManager {
SocketMessage::StaticConfigSchema => {
let socket_message = schema_for!(StaticConfig);
let schema = serde_json::to_string_pretty(&socket_message)?;
let socket = DATA_DIR.join("komorebic.sock");
let mut socket = DATA_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(socket)?;
stream.write_all(schema.as_bytes())?;
}
SocketMessage::GenerateStaticConfig => {
let config = serde_json::to_string_pretty(&StaticConfig::from(&*self))?;
let socket = DATA_DIR.join("komorebic.sock");
let mut socket = DATA_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(socket)?;
stream.write_all(config.as_bytes())?;
@@ -1278,7 +1182,6 @@ impl WindowManager {
match message {
SocketMessage::ChangeLayout(_)
| SocketMessage::CycleLayout(_)
| SocketMessage::ChangeLayoutCustom(_)
| SocketMessage::FlipLayout(_)
| SocketMessage::ManageFocusedWindow
@@ -1307,7 +1210,7 @@ impl WindowManager {
| SocketMessage::CycleMoveWindow(_)
| SocketMessage::MoveWindow(_) => {
let foreground = WindowsApi::foreground_window()?;
let foreground_window = Window::new(foreground);
let foreground_window = Window { hwnd: foreground };
let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?;
rect.top -= self.invisible_borders.bottom;
rect.bottom += self.invisible_borders.bottom;
@@ -1438,8 +1341,7 @@ pub fn read_commands_tcp(
break;
}
Ok(size) => {
let Ok(message) = SocketMessage::from_str(&String::from_utf8_lossy(&buf[..size]))
else {
let Ok(message) = SocketMessage::from_str(&String::from_utf8_lossy(&buf[..size])) else {
tracing::warn!("client sent an invalid message, disconnecting: {addr}");
let mut connections = TCP_CONNECTIONS.lock();
connections.remove(addr);

View File

@@ -15,7 +15,6 @@ use komorebi_core::WindowContainerBehaviour;
use crate::border::Border;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::window::should_act;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
@@ -30,7 +29,6 @@ use crate::BORDER_HIDDEN;
use crate::BORDER_HWND;
use crate::DATA_DIR;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
@@ -181,26 +179,15 @@ impl WindowManager {
{
let tray_and_multi_window_identifiers =
TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let title = &window.title()?;
let exe_name = &window.exe()?;
let class = &window.class()?;
// We don't want to purge windows that have been deliberately hidden by us, eg. when
// they are not on the top of a container stack.
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
let should_act = should_act(
title,
exe_name,
class,
&tray_and_multi_window_identifiers,
&regex_identifiers,
);
if !window.is_window()
|| should_act
|| !programmatically_hidden_hwnds.contains(&window.hwnd)
if ((!window.is_window()
|| tray_and_multi_window_identifiers.contains(&window.exe()?))
|| tray_and_multi_window_identifiers.contains(&window.class()?))
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
{
hide = true;
}
@@ -515,8 +502,7 @@ impl WindowManager {
WindowManagerEvent::DisplayChange(..)
| WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::Uncloak(..)
| WindowManagerEvent::UpdateFocusedWindowBorder(..) => {}
| WindowManagerEvent::Uncloak(..) => {}
};
if *self.focused_workspace()?.tile() && BORDER_ENABLED.load(Ordering::SeqCst) {
@@ -530,8 +516,7 @@ impl WindowManager {
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Hide(_, window)
| WindowManagerEvent::Minimize(_, window)
| WindowManagerEvent::UpdateFocusedWindowBorder(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;
@@ -597,10 +582,6 @@ impl WindowManager {
WindowsApi::invalidate_border_rect()?;
border.set_position(target_window, &self.invisible_borders, activate)?;
if matches!(event, WindowManagerEvent::UpdateFocusedWindowBorder(_)) {
window.focus(self.mouse_follows_focus)?;
}
if activate {
BORDER_HIDDEN.store(false, Ordering::SeqCst);
}

View File

@@ -31,7 +31,7 @@ pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
Event::MouseMoveRelative { .. } => {
if !ignore_movement {
match wm.lock().raise_window_at_cursor_pos() {
Ok(()) => {}
Ok(_) => {}
Err(error) => tracing::error!("{}", error),
}
}

View File

@@ -4,12 +4,8 @@ use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::ALT_FOCUS_HACK;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_EASE;
use crate::ANIMATION_ENABLED;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
@@ -26,23 +22,19 @@ use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use dirs::home_dir;
use hotwatch::notify::DebouncedEvent;
use hotwatch::Hotwatch;
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
use komorebi_core::config_generation::ApplicationOptions;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::resolve_home_path;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::DefaultLayout;
use komorebi_core::EaseEnum;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
use komorebi_core::Layout;
@@ -52,7 +44,6 @@ use komorebi_core::Rect;
use komorebi_core::SocketMessage;
use komorebi_core::WindowContainerBehaviour;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -76,16 +67,6 @@ pub struct Rgb {
pub b: u32,
}
impl From<u32> for Rgb {
fn from(value: u32) -> Self {
Self {
r: value & 0xff,
g: value >> 8 & 0xff,
b: value >> 16 & 0xff,
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ActiveWindowBorderColours {
/// Border colour when the container contains a single window
@@ -147,7 +128,6 @@ impl From<&Workspace> for WorkspaceConfig {
let rule = IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: identifier.clone(),
matching_strategy: None,
};
if *is_initial {
@@ -259,18 +239,12 @@ pub struct StaticConfig {
/// Path to applications.yaml from komorebi-application-specific-configurations (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub app_specific_configuration_path: Option<PathBuf>,
/// DEPRECATED from v0.1.19: use active_window_border_width instead
#[serde(skip_serializing_if = "Option::is_none")]
pub border_width: Option<i32>,
/// DEPRECATED from v0.1.19: use active_window_border_offset instead
#[serde(skip_serializing_if = "Option::is_none")]
pub border_offset: Option<Rect>,
/// Width of the active window border (default: 20)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_width: Option<i32>,
pub border_width: Option<i32>,
/// Offset of the active window border (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_offset: Option<i32>,
pub border_offset: Option<Rect>,
/// Display an active window border (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border: Option<bool>,
@@ -313,22 +287,9 @@ pub struct StaticConfig {
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change_applications: Option<Vec<IdWithIdentifier>>,
/// Set monitor index preferences
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
/// Enable or disable animations (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub animation: Option<bool>,
/// Set the animation ease function (default: Linear)
#[serde(skip_serializing_if = "Option::is_none")]
pub animation_ease: Option<EaseEnum>,
/// Set the animation duration in ms (default: 250)
#[serde(skip_serializing_if = "Option::is_none")]
pub animation_duration: Option<u64>,
}
impl From<&WindowManager> for StaticConfig {
#[allow(clippy::too_many_lines)]
fn from(value: &WindowManager) -> Self {
let default_invisible_borders = Rect {
left: 7,
@@ -387,24 +348,6 @@ impl From<&WindowManager> for StaticConfig {
}
}
let border_colours = if BORDER_COLOUR_SINGLE.load(Ordering::SeqCst) == 0 {
None
} else {
Option::from(ActiveWindowBorderColours {
single: Rgb::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
stack: Rgb::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
}),
monocle: Rgb::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
}),
})
};
Self {
invisible_borders: if value.invisible_borders == default_invisible_borders {
None
@@ -420,14 +363,10 @@ impl From<&WindowManager> for StaticConfig {
focus_follows_mouse: value.focus_follows_mouse,
mouse_follows_focus: Option::from(value.mouse_follows_focus),
app_specific_configuration_path: None,
active_window_border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
active_window_border_offset: BORDER_OFFSET
.lock()
.map_or(None, |offset| Option::from(offset.left)),
border_width: None,
border_offset: None,
border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: *BORDER_OFFSET.lock(),
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
active_window_border_colours: border_colours,
active_window_border_colours: None,
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
),
@@ -444,40 +383,18 @@ impl From<&WindowManager> for StaticConfig {
tray_and_multi_window_applications: None,
layered_applications: None,
object_name_change_applications: None,
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
animation: Option::from(ANIMATION_ENABLED.load(Ordering::SeqCst)),
animation_duration: Option::from(ANIMATION_DURATION.load(Ordering::SeqCst)),
animation_ease: Option::from(*ANIMATION_EASE.lock()),
}
}
}
impl StaticConfig {
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
fn apply_globals(&mut self) -> Result<()> {
if let Some(monitor_index_preferences) = &self.monitor_index_preferences {
let mut preferences = MONITOR_INDEX_PREFERENCES.lock();
*preferences = monitor_index_preferences.clone();
}
fn apply_globals(&self) -> Result<()> {
if let Some(behaviour) = self.window_hiding_behaviour {
let mut window_hiding_behaviour = HIDING_BEHAVIOUR.lock();
*window_hiding_behaviour = behaviour;
}
if let Some(animation) = self.animation {
ANIMATION_ENABLED.store(animation, Ordering::SeqCst);
}
if let Some(duration) = self.animation_duration {
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
}
if let Some(ease) = self.animation_ease {
let mut animation_ease = ANIMATION_EASE.lock();
*animation_ease = ease;
}
if let Some(hack) = self.alt_focus_hack {
ALT_FOCUS_HACK.store(hack, Ordering::SeqCst);
}
@@ -490,31 +407,14 @@ impl StaticConfig {
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
}
self.active_window_border_width.map_or_else(
|| {
BORDER_WIDTH.store(20, Ordering::SeqCst);
},
|width| {
BORDER_WIDTH.store(width, Ordering::SeqCst);
},
);
self.active_window_border_offset.map_or_else(
|| {
let mut border_offset = BORDER_OFFSET.lock();
*border_offset = None;
},
|offset| {
let new_border_offset = Rect {
left: offset,
top: offset,
right: offset * 2,
bottom: offset * 2,
};
if let Some(width) = self.border_width {
BORDER_WIDTH.store(width, Ordering::SeqCst);
}
let mut border_offset = BORDER_OFFSET.lock();
*border_offset = Some(new_border_offset);
},
);
if let Some(offset) = self.border_offset {
let mut border_offset = BORDER_OFFSET.lock();
*border_offset = Some(offset);
}
if let Some(colours) = &self.active_window_border_colours {
BORDER_COLOUR_SINGLE.store(
@@ -536,139 +436,75 @@ impl StaticConfig {
}
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
let mut border_overflow_identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
let mut layered_identifiers = LAYERED_WHITELIST.lock();
if let Some(float) = &mut self.float_rules {
if let Some(float) = &self.float_rules {
for identifier in float {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !float_identifiers.contains(identifier) {
float_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
if !float_identifiers.contains(&identifier.id) {
float_identifiers.push(identifier.id.clone());
}
}
}
if let Some(manage) = &mut self.manage_rules {
for identifier in manage {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !manage_identifiers.contains(identifier) {
manage_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
if let Some(float) = &self.manage_rules {
for identifier in float {
if !manage_identifiers.contains(&identifier.id) {
manage_identifiers.push(identifier.id.clone());
}
}
}
if let Some(identifiers) = &mut self.object_name_change_applications {
if let Some(identifiers) = &self.object_name_change_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !object_name_change_identifiers.contains(identifier) {
object_name_change_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
if !object_name_change_identifiers.contains(&identifier.id) {
object_name_change_identifiers.push(identifier.id.clone());
}
}
}
if let Some(identifiers) = &mut self.layered_applications {
if let Some(identifiers) = &self.layered_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !layered_identifiers.contains(identifier) {
layered_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
if !layered_identifiers.contains(&identifier.id) {
layered_identifiers.push(identifier.id.clone());
}
}
}
if let Some(identifiers) = &mut self.border_overflow_applications {
if let Some(identifiers) = &self.border_overflow_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !border_overflow_identifiers.contains(identifier) {
border_overflow_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
if !border_overflow_identifiers.contains(&identifier.id) {
border_overflow_identifiers.push(identifier.id.clone());
}
}
}
if let Some(identifiers) = &mut self.tray_and_multi_window_applications {
if let Some(identifiers) = &self.tray_and_multi_window_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !tray_and_multi_window_identifiers.contains(identifier) {
tray_and_multi_window_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
if !tray_and_multi_window_identifiers.contains(&identifier.id) {
tray_and_multi_window_identifiers.push(identifier.id.clone());
}
}
}
if let Some(path) = &self.app_specific_configuration_path {
let path = resolve_home_path(path)?;
let content = std::fs::read_to_string(path)?;
let stringified = path.to_string_lossy();
let stringified = stringified.replace(
"$Env:USERPROFILE",
&home_dir().expect("no home dir").to_string_lossy(),
);
let content = std::fs::read_to_string(stringified)?;
let asc = ApplicationConfigurationGenerator::load(&content)?;
for mut entry in asc {
for entry in asc {
if let Some(float) = entry.float_identifiers {
for f in float {
let mut without_comment: IdWithIdentifier = f.into();
if without_comment.matching_strategy.is_none() {
without_comment.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !float_identifiers.contains(&without_comment) {
float_identifiers.push(without_comment.clone());
if matches!(
without_comment.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&without_comment.id)?;
regex_identifiers.insert(without_comment.id.clone(), re);
}
if !float_identifiers.contains(&f.id) {
float_identifiers.push(f.id.clone());
}
}
}
@@ -676,94 +512,31 @@ impl StaticConfig {
for o in options {
match o {
ApplicationOptions::ObjectNameChange => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !object_name_change_identifiers.contains(&entry.identifier) {
object_name_change_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
if !object_name_change_identifiers.contains(&entry.identifier.id) {
object_name_change_identifiers
.push(entry.identifier.id.clone());
}
}
ApplicationOptions::Layered => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !layered_identifiers.contains(&entry.identifier) {
layered_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
if !layered_identifiers.contains(&entry.identifier.id) {
layered_identifiers.push(entry.identifier.id.clone());
}
}
ApplicationOptions::BorderOverflow => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !border_overflow_identifiers.contains(&entry.identifier) {
border_overflow_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
if !border_overflow_identifiers.contains(&entry.identifier.id) {
border_overflow_identifiers.push(entry.identifier.id.clone());
}
}
ApplicationOptions::TrayAndMultiWindow => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !tray_and_multi_window_identifiers.contains(&entry.identifier) {
if !tray_and_multi_window_identifiers.contains(&entry.identifier.id)
{
tray_and_multi_window_identifiers
.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
.push(entry.identifier.id.clone());
}
}
ApplicationOptions::Force => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !manage_identifiers.contains(&entry.identifier) {
manage_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
if !manage_identifiers.contains(&entry.identifier.id) {
manage_identifiers.push(entry.identifier.id.clone());
}
}
}
@@ -781,13 +554,13 @@ impl StaticConfig {
incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>,
) -> Result<WindowManager> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
let value: Self = serde_json::from_str(&content)?;
value.apply_globals()?;
let socket = DATA_DIR.join("komorebi.sock");
match std::fs::remove_file(&socket) {
Ok(()) => {}
Ok(_) => {}
Err(error) => match error.kind() {
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
ErrorKind::NotFound => {}
@@ -831,14 +604,6 @@ impl StaticConfig {
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
};
match value.focus_follows_mouse {
None => WindowsApi::disable_focus_follows_mouse()?,
Some(FocusFollowsMouseImplementation::Windows) => {
WindowsApi::enable_focus_follows_mouse()?;
}
Some(FocusFollowsMouseImplementation::Komorebi) => {}
};
let bytes = SocketMessage::ReloadStaticConfiguration(path.clone()).as_bytes()?;
wm.hotwatch.watch(path, move |event| match event {
@@ -909,7 +674,7 @@ impl StaticConfig {
pub fn reload(path: &PathBuf, wm: &mut WindowManager) -> Result<()> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
let value: Self = serde_json::from_str(&content)?;
value.apply_globals()?;
@@ -982,15 +747,6 @@ impl StaticConfig {
}
wm.work_area_offset = value.global_work_area_offset;
match value.focus_follows_mouse {
None => WindowsApi::disable_focus_follows_mouse()?,
Some(FocusFollowsMouseImplementation::Windows) => {
WindowsApi::enable_focus_follows_mouse()?;
}
Some(FocusFollowsMouseImplementation::Komorebi) => {}
};
wm.focus_follows_mouse = value.focus_follows_mouse;
Ok(())

View File

@@ -1,20 +1,12 @@
use crate::com::SetCloak;
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
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::Ordering;
use std::time::Duration;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingStrategy;
use regex::Regex;
use schemars::JsonSchema;
use serde::ser::Error;
use serde::ser::SerializeStruct;
@@ -25,11 +17,9 @@ use winput::press;
use winput::release;
use winput::Vk;
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::window_manager_event::WindowManagerEvent;
@@ -43,13 +33,11 @@ use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::NO_TITLEBAR;
use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy, JsonSchema)]
pub struct Window {
pub(crate) hwnd: isize,
animation: Animation,
}
impl Display for Window {
@@ -109,14 +97,6 @@ impl Serialize for Window {
}
impl Window {
// for instantiation of animation struct
pub fn new(hwnd: isize) -> Self {
Self {
hwnd,
animation: Animation::default(),
}
}
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
@@ -136,47 +116,6 @@ impl Window {
true,
)
}
pub fn animate_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
let hwnd = self.hwnd();
let curr_rect = WindowsApi::window_rect(hwnd).unwrap();
if curr_rect.left == layout.left
&& curr_rect.top == layout.top
&& curr_rect.bottom == layout.bottom
&& curr_rect.right == layout.right
{
WindowsApi::position_window(hwnd, layout, top)
} else {
let target_rect = *layout;
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
let mut animation = self.animation;
let self_copied = *self;
std::thread::spawn(move || {
animation.animate(duration, |progress: f64| {
let new_rect = Animation::lerp_rect(&curr_rect, &target_rect, progress);
if progress < 1.0 {
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(hwnd, &new_rect, true)?;
} else {
WindowsApi::position_window(hwnd, &new_rect, top)?;
if WindowsApi::foreground_window()? == self_copied.hwnd {
WINEVENT_CALLBACK_CHANNEL
.lock()
.0
.send(WindowManagerEvent::UpdateFocusedWindowBorder(self_copied))?;
}
}
Ok(())
})
});
Ok(())
}
}
pub fn set_position(
&mut self,
@@ -185,21 +124,15 @@ impl Window {
top: bool,
) -> Result<()> {
let mut rect = *layout;
let mut should_remove_border = true;
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let title = &self.title()?;
let class = &self.class()?;
let exe_name = &self.exe()?;
let should_remove_border = !should_act(
title,
exe_name,
class,
&border_overflows,
&regex_identifiers,
);
if border_overflows.contains(&self.title()?)
|| border_overflows.contains(&self.exe()?)
|| border_overflows.contains(&self.class()?)
{
should_remove_border = false;
}
if should_remove_border {
// Remove the invisible borders
@@ -209,17 +142,7 @@ impl Window {
rect.bottom += invisible_borders.bottom;
}
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
// check if animation is in progress
if self.animation.in_progress {
// wait for cancel animation
self.animation.cancel();
}
self.animate_position(&rect, top)
} else {
WindowsApi::position_window(self.hwnd(), &rect, top)
}
WindowsApi::position_window(self.hwnd(), &rect, top)
}
pub fn hide(self) {
@@ -306,7 +229,7 @@ impl Window {
// Raise Window to foreground
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {}
Ok(_) => {}
Err(error) => {
tracing::error!(
"could not set as foreground window, but continuing execution of raise(): {}",
@@ -317,7 +240,7 @@ impl Window {
// This isn't really needed when the above command works as expected via AHK
match WindowsApi::set_focus(self.hwnd()) {
Ok(()) => {}
Ok(_) => {}
Err(error) => {
tracing::error!(
"could not set focus, but continuing execution of raise(): {}",
@@ -367,7 +290,7 @@ impl Window {
}
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {
Ok(_) => {
foregrounded = true;
}
Err(error) => {
@@ -399,7 +322,7 @@ impl Window {
// This isn't really needed when the above command works as expected via AHK
match WindowsApi::set_focus(self.hwnd()) {
Ok(()) => {}
Ok(_) => {}
Err(error) => {
tracing::error!(
"could not set focus, but continuing execution of focus(): {}",
@@ -425,7 +348,8 @@ 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())
WindowsApi::set_transparent(self.hwnd());
Ok(())
}
pub fn opaque(self) -> Result<()> {
@@ -546,38 +470,42 @@ fn window_is_eligible(
}
}
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut should_float = false;
let float_identifiers = FLOAT_IDENTIFIERS.lock();
let should_float = should_act(
title,
exe_name,
class,
&float_identifiers,
&regex_identifiers,
);
{
let float_identifiers = FLOAT_IDENTIFIERS.lock();
for identifier in float_identifiers.iter() {
if title.starts_with(identifier) || title.ends_with(identifier) {
should_float = true;
}
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
let managed_override = should_act(
title,
exe_name,
class,
&manage_identifiers,
&regex_identifiers,
);
if class.starts_with(identifier) || class.ends_with(identifier) {
should_float = true;
}
if identifier == exe_name {
should_float = true;
}
}
};
let managed_override = {
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
manage_identifiers.contains(exe_name)
|| manage_identifiers.contains(class)
|| manage_identifiers.contains(title)
};
if should_float && !managed_override {
return false;
}
let layered_whitelist = LAYERED_WHITELIST.lock();
let allow_layered = should_act(
title,
exe_name,
class,
&layered_whitelist,
&regex_identifiers,
);
let allow_layered = {
let layered_whitelist = LAYERED_WHITELIST.lock();
layered_whitelist.contains(exe_name)
|| layered_whitelist.contains(class)
|| layered_whitelist.contains(title)
};
// TODO: might need this for transparency
// let allow_layered = true;
@@ -608,131 +536,3 @@ fn window_is_eligible(
false
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
pub fn should_act(
title: &str,
exe_name: &str,
class: &str,
identifiers: &[IdWithIdentifier],
regex_identifiers: &HashMap<String, Regex>,
) -> bool {
let mut should_act = false;
for identifier in identifiers {
match identifier.matching_strategy {
None => {
panic!("there is no matching strategy identified for this rule");
}
Some(MatchingStrategy::Legacy) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Equals) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::StartsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.starts_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::EndsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.ends_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Contains) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.contains(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Regex) => match identifier.kind {
ApplicationIdentifier::Title => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(title) {
should_act = true;
}
}
}
ApplicationIdentifier::Class => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(class) {
should_act = true;
}
}
}
ApplicationIdentifier::Exe => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(exe_name) {
should_act = true;
}
}
}
},
}
}
should_act
}

View File

@@ -3,13 +3,11 @@ use std::collections::HashSet;
use std::collections::VecDeque;
use std::io::ErrorKind;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::notify::DebouncedEvent;
@@ -19,7 +17,6 @@ use schemars::JsonSchema;
use serde::Serialize;
use uds_windows::UnixListener;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::custom_layout::CustomLayout;
use komorebi_core::Arrangement;
use komorebi_core::Axis;
@@ -53,7 +50,6 @@ use crate::FLOAT_IDENTIFIERS;
use crate::HOME_DIR;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
@@ -96,13 +92,12 @@ pub struct State {
pub mouse_follows_focus: bool,
pub has_pending_raise_op: bool,
pub remove_titlebars: bool,
pub float_identifiers: Vec<IdWithIdentifier>,
pub manage_identifiers: Vec<IdWithIdentifier>,
pub layered_whitelist: Vec<IdWithIdentifier>,
pub tray_and_multi_window_identifiers: Vec<IdWithIdentifier>,
pub border_overflow_identifiers: Vec<IdWithIdentifier>,
pub name_change_on_launch_identifiers: Vec<IdWithIdentifier>,
pub monitor_index_preferences: HashMap<usize, Rect>,
pub float_identifiers: Vec<String>,
pub manage_identifiers: Vec<String>,
pub layered_whitelist: Vec<String>,
pub tray_and_multi_window_identifiers: Vec<String>,
pub border_overflow_identifiers: Vec<String>,
pub name_change_on_launch_identifiers: Vec<String>,
}
impl AsRef<Self> for WindowManager {
@@ -131,7 +126,6 @@ impl From<&WindowManager> for State {
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
border_overflow_identifiers: BORDER_OVERFLOW_IDENTIFIERS.lock().clone(),
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
}
}
}
@@ -168,7 +162,7 @@ impl WindowManager {
let socket = DATA_DIR.join("komorebi.sock");
match std::fs::remove_file(&socket) {
Ok(()) => {}
Ok(_) => {}
Err(error) => match error.kind() {
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
ErrorKind::NotFound => {}
@@ -217,7 +211,7 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn show_border(&self) -> Result<()> {
let foreground = WindowsApi::foreground_window()?;
let foreground_window = Window::new(foreground);
let foreground_window = Window { hwnd: foreground };
let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?;
rect.top -= self.invisible_borders.bottom;
rect.bottom += self.invisible_borders.bottom;
@@ -249,8 +243,13 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn watch_configuration(&mut self, enable: bool) -> Result<()> {
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");
let home = HOME_DIR.clone();
let mut config_pwsh = home.clone();
config_pwsh.push("komorebi.ps1");
let mut config_ahk = home;
config_ahk.push("komorebi.ahk");
if config_pwsh.exists() {
self.configure_watcher(enable, config_pwsh)?;
@@ -262,39 +261,50 @@ impl WindowManager {
}
fn configure_watcher(&mut self, enable: bool, config: PathBuf) -> Result<()> {
if enable {
tracing::info!("watching configuration for changes: {}", config.display());
// Always make absolutely sure that there isn't an already existing watch, because
// hotwatch allows multiple watches to be registered for the same path
match self.hotwatch.unwatch(&config) {
Ok(()) => {}
Err(error) => match error {
hotwatch::Error::Notify(error) => match error {
hotwatch::notify::Error::WatchNotFound => {}
error => return Err(error.into()),
if config.exists() {
if enable {
tracing::info!(
"watching configuration for changes: {}",
config
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
// Always make absolutely sure that there isn't an already existing watch, because
// hotwatch allows multiple watches to be registered for the same path
match self.hotwatch.unwatch(config.clone()) {
Ok(_) => {}
Err(error) => match error {
hotwatch::Error::Notify(error) => match error {
hotwatch::notify::Error::WatchNotFound => {}
error => return Err(error.into()),
},
error @ hotwatch::Error::Io(_) => return Err(error.into()),
},
error @ hotwatch::Error::Io(_) => return Err(error.into()),
},
}
self.hotwatch.watch(config, |event| match event {
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
// a NoticeRemove, presumably because of the use of swap files?
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {
std::thread::spawn(|| {
load_configuration().expect("could not load configuration");
});
}
_ => {}
})?;
} else {
tracing::info!(
"no longer watching configuration for changes: {}",
config.display()
);
self.hotwatch.unwatch(config)?;
};
self.hotwatch.watch(config, |event| match event {
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
// a NoticeRemove, presumably because of the use of swap files?
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {
std::thread::spawn(|| {
load_configuration().expect("could not load configuration");
});
}
_ => {}
})?;
} else {
tracing::info!(
"no longer watching configuration for changes: {}",
config
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
self.hotwatch.unwatch(config)?;
};
}
Ok(())
}
@@ -589,7 +599,7 @@ impl WindowManager {
// Hide the window we are about to remove if it is on the currently focused workspace
if op.is_origin(focused_monitor_idx, focused_workspace_idx) {
Window::new(op.hwnd).hide();
Window { hwnd: op.hwnd }.hide();
should_update_focused_workspace = true;
}
@@ -619,7 +629,7 @@ impl WindowManager {
.get_mut(op.target_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
target_workspace.new_container_for_window(Window::new(op.hwnd));
target_workspace.new_container_for_window(Window { hwnd: op.hwnd });
}
// Only re-tile the focused workspace if we need to
@@ -663,14 +673,14 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn manage_focused_window(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let event = WindowManagerEvent::Manage(Window::new(hwnd));
let event = WindowManagerEvent::Manage(Window { hwnd });
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
}
#[tracing::instrument(skip(self))]
pub fn unmanage_focused_window(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let event = WindowManagerEvent::Unmanage(Window::new(hwnd));
let event = WindowManagerEvent::Unmanage(Window { hwnd });
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
}
@@ -708,14 +718,13 @@ impl WindowManager {
];
if !known_hwnd {
let class = Window::new(hwnd).class()?;
let class = Window { hwnd }.class()?;
// Some applications (Electron/Chromium-based, explorer) have (invisible?) overlays
// windows that we need to look beyond to find the actual window to raise
if overlay_classes.contains(&class) {
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
if let Some(exe_hwnd) =
workspace.hwnd_from_exe(&Window::new(hwnd).exe()?)
if let Some(exe_hwnd) = workspace.hwnd_from_exe(&Window { hwnd }.exe()?)
{
hwnd = exe_hwnd;
known_hwnd = true;
@@ -726,11 +735,11 @@ impl WindowManager {
}
if known_hwnd {
let event = WindowManagerEvent::Raise(Window::new(hwnd));
let event = WindowManagerEvent::Raise(Window { hwnd });
self.has_pending_raise_op = true;
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
} else {
tracing::debug!("not raising unknown window: {}", Window::new(hwnd,));
tracing::debug!("not raising unknown window: {}", Window { hwnd });
Ok(())
}
}
@@ -844,7 +853,9 @@ impl WindowManager {
} else if let Ok(window) = self.focused_window_mut() {
window.focus(self.mouse_follows_focus)?;
} else {
let desktop_window = Window::new(WindowsApi::desktop_window()?);
let desktop_window = Window {
hwnd: WindowsApi::desktop_window()?,
};
let rect = self.focused_monitor_size()?;
WindowsApi::center_cursor_in_rect(&rect)?;
@@ -853,7 +864,7 @@ impl WindowManager {
// attach to the thread of the desktop window always seems to result in "Access is
// denied (os error 5)"
match WindowsApi::set_foreground_window(desktop_window.hwnd()) {
Ok(()) => {}
Ok(_) => {}
Err(error) => {
tracing::warn!("{} {}:{}", error, file!(), line!());
}
@@ -987,7 +998,9 @@ impl WindowManager {
let workspace = self.focused_workspace()?;
let focused_hwnd = WindowsApi::foreground_window()?;
if !workspace.contains_managed_window(focused_hwnd) {
bail!("ignoring commands while active window is not managed by komorebi");
return Err(anyhow!(
"ignoring commands while active window is not managed by komorebi"
));
}
}
@@ -1103,7 +1116,9 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no workspace"))?;
if workspace.maximized_window().is_some() {
bail!("cannot move native maximized window to another monitor or workspace");
return Err(anyhow!(
"cannot move native maximized window to another monitor or workspace"
));
}
let container = workspace
@@ -1213,7 +1228,9 @@ impl WindowManager {
// removing this messes up the monitor / container / window index somewhere
// and results in the wrong window getting moved across the monitor boundary
if workspace.is_focused_window_monocle_or_maximized()? {
bail!("ignoring command while active window is in monocle mode or maximized");
return Err(anyhow!(
"ignoring command while active window is in monocle mode or maximized"
));
}
tracing::info!("moving container");
@@ -1375,7 +1392,9 @@ impl WindowManager {
let workspace = self.focused_workspace_mut()?;
if workspace.is_focused_window_monocle_or_maximized()? {
bail!("ignoring command while active window is in monocle mode or maximized");
return Err(anyhow!(
"ignoring command while active window is in monocle mode or maximized"
));
}
tracing::info!("moving container");
@@ -1402,7 +1421,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
if len.get() == 1 {
bail!("there is only one window in this container");
return Err(anyhow!("there is only one window in this container"));
}
let current_idx = container.focused_window_idx();
@@ -1487,7 +1506,7 @@ impl WindowManager {
tracing::info!("removing window");
if self.focused_container()?.windows().len() == 1 {
bail!("a container must have at least one window");
return Err(anyhow!("a container must have at least one window"));
}
let workspace = self.focused_workspace_mut()?;
@@ -1683,36 +1702,10 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn cycle_layout(&mut self, direction: CycleDirection) -> Result<()> {
tracing::info!("cycling layout");
let workspace = self.focused_workspace_mut()?;
let current_layout = workspace.layout();
match current_layout {
Layout::Default(current) => {
let new_layout = match direction {
CycleDirection::Previous => current.cycle_previous(),
CycleDirection::Next => current.cycle_next(),
};
tracing::info!("next layout: {new_layout}");
workspace.set_layout(Layout::Default(new_layout));
}
Layout::Custom(_) => {}
}
self.update_focused_workspace(self.mouse_follows_focus)
}
#[tracing::instrument(skip(self))]
pub fn change_workspace_custom_layout<P>(&mut self, path: P) -> Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
{
pub fn change_workspace_custom_layout(&mut self, path: PathBuf) -> Result<()> {
tracing::info!("changing layout");
let layout = CustomLayout::from_path(path)?;
let layout = CustomLayout::from_path_buf(path)?;
let workspace = self.focused_workspace_mut()?;
match workspace.layout() {
@@ -1834,16 +1827,13 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn add_workspace_layout_custom_rule<P>(
pub fn add_workspace_layout_custom_rule(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
at_container_count: usize,
path: P,
) -> Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
{
path: PathBuf,
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
@@ -1868,7 +1858,7 @@ impl WindowManager {
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let layout = CustomLayout::from_path(path)?;
let layout = CustomLayout::from_path_buf(path)?;
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
rules.retain(|pair| pair.0 != at_container_count);
@@ -1969,17 +1959,14 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn set_workspace_layout_custom<P>(
pub fn set_workspace_layout_custom(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
path: P,
) -> Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
{
path: PathBuf,
) -> Result<()> {
tracing::info!("setting workspace layout");
let layout = CustomLayout::from_path(path)?;
let layout = CustomLayout::from_path_buf(path)?;
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
@@ -2150,7 +2137,7 @@ impl WindowManager {
if self.monitors().get(idx).is_some() {
self.monitors.focus(idx);
} else {
bail!("this is not a valid monitor index");
return Err(anyhow!("this is not a valid monitor index"));
}
Ok(())

View File

@@ -4,11 +4,9 @@ use std::fmt::Formatter;
use schemars::JsonSchema;
use serde::Serialize;
use crate::window::should_act;
use crate::window::Window;
use crate::winevent::WinEvent;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
#[derive(Debug, Copy, Clone, Serialize, JsonSchema)]
#[serde(tag = "type", content = "content")]
@@ -27,7 +25,6 @@ pub enum WindowManagerEvent {
Unmanage(Window),
Raise(Window),
DisplayChange(Window),
UpdateFocusedWindowBorder(Window),
}
impl Display for WindowManagerEvent {
@@ -78,9 +75,6 @@ impl Display for WindowManagerEvent {
Self::DisplayChange(window) => {
write!(f, "DisplayChange (Window: {window})")
}
Self::UpdateFocusedWindowBorder(window) => {
write!(f, "UpdateFocusedBorderWindow (Window: {window})")
}
}
}
}
@@ -101,8 +95,7 @@ impl WindowManagerEvent {
| Self::Raise(window)
| Self::Manage(window)
| Self::DisplayChange(window)
| Self::Unmanage(window)
| Self::UpdateFocusedWindowBorder(window) => window,
| Self::Unmanage(window) => window,
}
}
@@ -138,21 +131,11 @@ impl WindowManagerEvent {
// [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let title = &window.title().ok()?;
let exe_name = &window.exe().ok()?;
let class = &window.class().ok()?;
let should_trigger = should_act(
title,
exe_name,
class,
&object_name_change_on_launch,
&regex_identifiers,
);
if should_trigger {
if object_name_change_on_launch.contains(&window.exe().ok()?)
|| object_name_change_on_launch.contains(&window.class().ok()?)
|| object_name_change_on_launch.contains(&window.title().ok()?)
{
Option::from(Self::Show(winevent, window))
} else {
None

View File

@@ -81,7 +81,6 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows::Win32::UI::WindowsAndMessaging::IsIconic;
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
use windows::Win32::UI::WindowsAndMessaging::MoveWindow;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use windows::Win32::UI::WindowsAndMessaging::RegisterClassA;
@@ -106,9 +105,7 @@ use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
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::SW_HIDE;
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
@@ -237,7 +234,9 @@ impl WindowsApi {
}
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
unsafe { EnumWindows(callback, LPARAM(callback_data_address)) }.process()
unsafe { EnumWindows(callback, LPARAM(callback_data_address)) }
.ok()
.process()
}
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
@@ -276,7 +275,9 @@ impl WindowsApi {
}
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
unsafe { AllowSetForegroundWindow(process_id) }.process()
unsafe { AllowSetForegroundWindow(process_id) }
.ok()
.process()
}
pub fn monitor_from_window(hwnd: HWND) -> isize {
@@ -312,7 +313,7 @@ impl WindowsApi {
}
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
unsafe { BringWindowToTop(hwnd) }.process()
unsafe { BringWindowToTop(hwnd) }.ok().process()
}
pub fn raise_window(hwnd: HWND) -> Result<()> {
@@ -352,20 +353,7 @@ impl WindowsApi {
SET_WINDOW_POS_FLAGS(flags),
)
}
.process()
}
pub fn move_window(hwnd: HWND, layout: &Rect, repaint: bool) -> Result<()> {
unsafe {
MoveWindow(
hwnd,
layout.left,
layout.top,
layout.right,
layout.bottom,
repaint,
)
}
.ok()
.process()
}
@@ -380,12 +368,14 @@ impl WindowsApi {
}
fn post_message(hwnd: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> Result<()> {
unsafe { PostMessageW(hwnd, message, wparam, lparam) }.process()
unsafe { PostMessageW(hwnd, message, wparam, lparam) }
.ok()
.process()
}
pub fn close_window(hwnd: HWND) -> Result<()> {
match Self::post_message(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)) {
Ok(()) => Ok(()),
Ok(_) => Ok(()),
Err(_) => Err(anyhow!("could not close window")),
}
}
@@ -446,18 +436,18 @@ impl WindowsApi {
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
let mut rect = unsafe { std::mem::zeroed() };
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
unsafe { GetWindowRect(hwnd, &mut rect) }.ok().process()?;
Ok(Rect::from(rect))
}
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
unsafe { SetCursorPos(x, y) }.process()
unsafe { SetCursorPos(x, y) }.ok().process()
}
pub fn cursor_pos() -> Result<POINT> {
let mut cursor_pos = POINT::default();
unsafe { GetCursorPos(&mut cursor_pos) }.process()?;
unsafe { GetCursorPos(&mut cursor_pos) }.ok().process()?;
Ok(cursor_pos)
}
@@ -499,7 +489,7 @@ impl WindowsApi {
let mut session_id = 0;
unsafe {
if ProcessIdToSessionId(process_id, &mut session_id).is_ok() {
if ProcessIdToSessionId(process_id, &mut session_id).as_bool() {
Ok(session_id)
} else {
Err(anyhow!("could not determine current session id"))
@@ -575,7 +565,7 @@ impl WindowsApi {
}
pub fn close_process(handle: HANDLE) -> Result<()> {
unsafe { CloseHandle(handle) }.process()
unsafe { CloseHandle(handle) }.ok().process()
}
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
@@ -590,6 +580,7 @@ impl WindowsApi {
unsafe {
QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, PWSTR(text_ptr), &mut len)
}
.ok()
.process()?;
Ok(String::from_utf16(&path[..len as usize])?)
@@ -682,6 +673,7 @@ impl WindowsApi {
pub fn set_process_dpi_awareness_context() -> Result<()> {
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }
.ok()
.process()
}
@@ -693,45 +685,10 @@ impl WindowsApi {
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
) -> Result<()> {
unsafe { SystemParametersInfoW(action, ui_param, Option::from(pv_param), update_flags) }
.ok()
.process()
}
#[tracing::instrument]
pub fn foreground_lock_timeout() -> Result<()> {
let mut value: u32 = 0;
Self::system_parameters_info_w(
SPI_GETFOREGROUNDLOCKTIMEOUT,
0,
std::ptr::addr_of_mut!(value).cast(),
SPIF_SENDCHANGE,
)?;
tracing::info!("current value of ForegroundLockTimeout is {value}");
if value != 0 {
tracing::info!("updating value of ForegroundLockTimeout to {value} in order to enable keyboard-driven focus updating");
Self::system_parameters_info_w(
SPI_SETFOREGROUNDLOCKTIMEOUT,
0,
std::ptr::null_mut::<c_void>(),
SPIF_SENDCHANGE,
)?;
Self::system_parameters_info_w(
SPI_GETFOREGROUNDLOCKTIMEOUT,
0,
std::ptr::addr_of_mut!(value).cast(),
SPIF_SENDCHANGE,
)?;
tracing::info!("updated value of ForegroundLockTimeout is now {value}");
}
Ok(())
}
#[allow(dead_code)]
pub fn focus_follows_mouse() -> Result<bool> {
let mut is_enabled: BOOL = unsafe { std::mem::zeroed() };
@@ -820,21 +777,19 @@ impl WindowsApi {
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY);
hwnd
}
.process()
}
pub fn set_transparent(hwnd: HWND) -> Result<()> {
pub fn set_transparent(hwnd: HWND) {
unsafe {
#[allow(clippy::cast_sign_loss)]
// TODO: alpha should be configurable
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), 150, LWA_ALPHA)?;
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), 150, LWA_ALPHA);
}
Ok(())
}
pub fn create_hidden_window(name: PCSTR, instance: HMODULE) -> Result<isize> {

View File

@@ -104,7 +104,7 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let is_minimized = WindowsApi::is_iconic(hwnd);
if is_visible && is_window && !is_minimized {
let window = Window::new(hwnd.0);
let window = Window { hwnd: hwnd.0 };
if let Ok(should_manage) = window.should_manage(None) {
if should_manage {
@@ -132,7 +132,7 @@ pub extern "system" fn win_event_hook(
return;
}
let window = Window::new(hwnd.0);
let window = Window { hwnd: hwnd.0 };
let winevent = unsafe { ::std::mem::transmute(event) };
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
@@ -196,7 +196,7 @@ pub extern "system" fn hidden_window(
unsafe {
match message {
WM_DISPLAYCHANGE => {
let event_type = WindowManagerEvent::DisplayChange(Window::new(window.0));
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
WINEVENT_CALLBACK_CHANNEL
.lock()
.0
@@ -211,7 +211,7 @@ pub extern "system" fn hidden_window(
if wparam.0 as u32 == SPI_SETWORKAREA.0
|| wparam.0 as u32 == SPI_ICONVERTICALSPACING.0
{
let event_type = WindowManagerEvent::DisplayChange(Window::new(window.0));
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
WINEVENT_CALLBACK_CHANNEL
.lock()
.0
@@ -224,7 +224,7 @@ pub extern "system" fn hidden_window(
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
let event_type = WindowManagerEvent::DisplayChange(Window::new(window.0));
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
WINEVENT_CALLBACK_CHANNEL
.lock()
.0

View File

@@ -61,7 +61,7 @@ impl WinEventListener {
MessageLoop::start(10, |_msg| {
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().1.try_recv() {
match outgoing.send(event) {
Ok(()) => {}
Ok(_) => {}
Err(error) => {
tracing::error!("{}", error);
}

View File

@@ -19,15 +19,11 @@ use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use crate::border::Border;
use crate::container::Container;
use crate::ring::Ring;
use crate::static_config::WorkspaceConfig;
use crate::window::Window;
use crate::windows_api::WindowsApi;
use crate::ANIMATION_ENABLED;
use crate::BORDER_HIDDEN;
use crate::BORDER_HWND;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
@@ -108,17 +104,11 @@ impl Workspace {
if let Some(layout) = &config.layout {
self.layout = Layout::Default(*layout);
self.tile = true;
}
if let Some(pathbuf) = &config.custom_layout {
let layout = CustomLayout::from_path(pathbuf)?;
let layout = CustomLayout::from_path_buf(pathbuf.clone())?;
self.layout = Layout::Custom(layout);
self.tile = true;
}
if config.custom_layout.is_none() && config.layout.is_none() {
self.tile = false;
}
if let Some(layout_rules) = &config.layout_rules {
@@ -133,7 +123,7 @@ impl Workspace {
if let Some(layout_rules) = &config.custom_layout_rules {
let rules = self.layout_rules_mut();
for (count, pathbuf) in layout_rules {
let rule = CustomLayout::from_path(pathbuf)?;
let rule = CustomLayout::from_path_buf(pathbuf.clone())?;
rules.push((*count, Layout::Custom(rule)));
}
}
@@ -248,12 +238,6 @@ impl Workspace {
}
if *self.tile() {
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window_mut() {
adjusted_work_area.add_padding(container_padding);
@@ -791,16 +775,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::UltrawideVerticalStack) => {
self.enforce_resize_for_ultrawide();
}
_ => self.enforce_no_resize(),
}
}
fn enforce_resize_constraints_for_bsp(&mut self) {
for (i, rect) in self.resize_dimensions_mut().iter_mut().enumerate() {
if let Some(rect) = rect {
// Even containers can't be resized to the bottom
@@ -826,72 +800,6 @@ impl Workspace {
}
}
fn enforce_resize_for_ultrawide(&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(),
// Two windows can only be resized in the middle
2 => {
// Zero is actually on the right
if let Some(mut right) = resize_dimensions[0] {
right.top = 0;
right.bottom = 0;
right.right = 0;
}
// One is on the left
if let Some(mut left) = resize_dimensions[1] {
left.top = 0;
left.bottom = 0;
left.left = 0;
}
}
// Three or more windows means 0 is in center, 1 is at the left, 2.. are a vertical
// stack on the right
_ => {
// Central can be resized left or right
if let Some(mut right) = resize_dimensions[0] {
right.top = 0;
right.bottom = 0;
}
// Left one can only be resized to the right
if let Some(mut left) = resize_dimensions[1] {
left.top = 0;
left.bottom = 0;
left.left = 0;
}
// Handle stack on the right
let stack_size = resize_dimensions[2..].len();
for (i, rect) in resize_dimensions[2..].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_no_resize(&mut self) {
for rect in self.resize_dimensions_mut().iter_mut().flatten() {
rect.left = 0;
rect.right = 0;
rect.top = 0;
rect.bottom = 0;
}
}
pub fn new_monocle_container(&mut self) -> Result<()> {
let focused_idx = self.focused_container_idx();
let container = self

View File

@@ -1,15 +0,0 @@
[package]
name = "komorebic-no-console"
version = "0.1.19"
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"]
repository = "https://github.com/LGUG2Z/komorebi"
license = "MIT"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -1,19 +0,0 @@
#![windows_subsystem = "windows"]
use std::io;
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
fn main() -> io::Result<()> {
let mut current_exe = std::env::current_exe().expect("unable to get exec path");
current_exe.pop();
let komorebic_exe = current_exe.join("komorebic.exe");
Command::new(komorebic_exe)
.args(std::env::args_os().skip(1))
.creation_flags(CREATE_NO_WINDOW)
.status()
.map(|_| ())
}

View File

@@ -196,10 +196,6 @@ ChangeLayout(default_layout) {
RunWait("komorebic.exe change-layout " default_layout, , "Hide")
}
CycleLayout(operation_direction) {
RunWait("komorebic.exe cycle-layout " operation_direction, , "Hide")
}
LoadCustomLayout(path) {
RunWait("komorebic.exe load-custom-layout " path, , "Hide")
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.19"
version = "0.1.16"
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"]
@@ -15,6 +15,8 @@ derive-ahk = { path = "../derive-ahk" }
komorebi-core = { path = "../komorebi-core" }
clap = { version = "4", features = ["derive", "wrap_help"] }
color-eyre = "0.6"
dirs = "5"
fs-tail = "0.1"
heck = "0.4"
lazy_static = "1"
@@ -26,8 +28,5 @@ serde_json = "1"
serde_yaml = "0.9"
sysinfo = "0.29"
uds_windows = "1"
which = "5"
windows = { workspace = true }
color-eyre = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
which = "4"
windows = { workspace = true }

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Array_of_ApplicationConfiguration",
"type": "array",
"items": {
"$ref": "#/definitions/ApplicationConfiguration"
},
"definitions": {
"ApplicationConfiguration": {
"type": "object",
"required": [
"identifier",
"name"
],
"properties": {
"float_identifiers": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/IdWithIdentifierAndComment"
}
},
"identifier": {
"$ref": "#/definitions/IdWithIdentifier"
},
"name": {
"type": "string"
},
"options": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/ApplicationOptions"
}
}
}
},
"ApplicationIdentifier": {
"type": "string",
"enum": [
"Exe",
"Class",
"Title"
]
},
"ApplicationOptions": {
"type": "string",
"enum": [
"object_name_change",
"layered",
"border_overflow",
"tray_and_multi_window",
"force"
]
},
"IdWithIdentifier": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/ApplicationIdentifier"
},
"matching_strategy": {
"anyOf": [
{
"$ref": "#/definitions/MatchingStrategy"
},
{
"type": "null"
}
]
}
}
},
"IdWithIdentifierAndComment": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"comment": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/ApplicationIdentifier"
},
"matching_strategy": {
"anyOf": [
{
"$ref": "#/definitions/MatchingStrategy"
},
{
"type": "null"
}
]
}
}
},
"MatchingStrategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex"
]
}
}
}

View File

@@ -21,22 +21,6 @@
}
]
},
"active_window_border_offset": {
"description": "Offset of the active window border (default: None)",
"type": [
"integer",
"null"
],
"format": "int32"
},
"active_window_border_width": {
"description": "Width of the active window border (default: 20)",
"type": [
"integer",
"null"
],
"format": "int32"
},
"alt_focus_hack": {
"description": "Always send the ALT key when using focus commands (default: false)",
"type": [
@@ -52,7 +36,7 @@
]
},
"border_offset": {
"description": "DEPRECATED from v0.1.19: use active_window_border_offset instead",
"description": "Offset of the active window border (default: None)",
"anyOf": [
{
"$ref": "#/definitions/Rect"
@@ -73,7 +57,7 @@
}
},
"border_width": {
"description": "DEPRECATED from v0.1.19: use active_window_border_width instead",
"description": "Width of the active window border (default: 20)",
"type": [
"integer",
"null"
@@ -170,16 +154,6 @@
"$ref": "#/definitions/IdWithIdentifier"
}
},
"monitor_index_preferences": {
"description": "Set monitor index preferences",
"type": [
"object",
"null"
],
"additionalProperties": {
"$ref": "#/definitions/Rect"
}
},
"monitors": {
"description": "Monitor and workspace configurations",
"type": [
@@ -368,30 +342,9 @@
},
"kind": {
"$ref": "#/definitions/ApplicationIdentifier"
},
"matching_strategy": {
"anyOf": [
{
"$ref": "#/definitions/MatchingStrategy"
},
{
"type": "null"
}
]
}
}
},
"MatchingStrategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex"
]
},
"MonitorConfig": {
"type": "object",
"required": [
@@ -618,4 +571,4 @@
}
}
}
}
}

View File

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