Compare commits

..

29 Commits

Author SHA1 Message Date
LGUG2Z
6cf99a3578 feat(config): initial outline of composite rules 2024-03-20 12:24:12 -07:00
LGUG2Z
ca22cdb07f fix(wm): add hack for new firefox windows
This commit adds a 10 millisecond thread sleep specifically for Firefox
as an interim solution for the race condition which results in new
windows often not being tiled correctly until interacted with.

The idea to add a sleep instead of spamming the output with dbg! calls
comes from @kornel@mastodon.social:

https://mastodon.social/@kornel/112125851048707993
2024-03-19 20:27:25 -07:00
dependabot[bot]
dc38eae2af chore(deps): bump serde_yaml from 0.9.32 to 0.9.33
Bumps [serde_yaml](https://github.com/dtolnay/serde-yaml) from 0.9.32 to 0.9.33.
- [Release notes](https://github.com/dtolnay/serde-yaml/releases)
- [Commits](https://github.com/dtolnay/serde-yaml/compare/0.9.32...0.9.33)

---
updated-dependencies:
- dependency-name: serde_yaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 13:24:46 -07:00
dependabot[bot]
66446f571c chore(deps): bump os_info from 3.8.0 to 3.8.1
Bumps [os_info](https://github.com/stanislav-tkach/os_info) from 3.8.0 to 3.8.1.
- [Release notes](https://github.com/stanislav-tkach/os_info/releases)
- [Changelog](https://github.com/stanislav-tkach/os_info/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stanislav-tkach/os_info/compare/v3.8.0...v3.8.1)

---
updated-dependencies:
- dependency-name: os_info
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 13:18:37 -07:00
LGUG2Z
b2f6329963 fix(wm): raise applicationframehost apps via state
This commit ensures that ApplicationFrameHost.exe applications developed
by Microsoft are raised correctly when a user has the custom komorebi
ffm implementation enabled.

The Win32 API calls EnumWindows and WindowFromPoint return different
HWNDs for the same windows because of some jank in how the
ApplicationFrameHost apps are developed.

To avoid this inconsistency on the Win32 API level, komorebi now queries
its own state when looking up HWNDs for windows at any given cursor
position.
2024-03-16 15:35:37 -07:00
LGUG2Z
bc46f65f64 fix(wm): use focus fn in komorebi ffm
This commit swaps out the old "raise" fn for the more up-to-date and
tested "focus" fn when raising a window for focus when the "komorebi"
implementation of focus follows mouse is enabled.
2024-03-15 18:57:38 -07:00
LGUG2Z
69573c383f refactor(wm): use notopmost flag instead of bottom
It seems like when we use the bottom flag, Rainmeter widgets will be
drawn on top of windows that are managed by komorebi. After looking at
the GlazeWM codebase, where this issue does not occur, it seems like the
difference is made by the use of the notopmost flag.

resolve #679
2024-03-15 18:48:53 -07:00
LGUG2Z
45a3f2a6b5 chore(dev): begin v0.1.23-dev 2024-03-15 18:48:28 -07:00
dependabot[bot]
5af00b64cf chore(deps): bump softprops/action-gh-release from 1 to 2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:27:03 -07:00
dependabot[bot]
81dff3279c chore(deps): bump clap from 4.5.1 to 4.5.2
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:26:14 -07:00
dependabot[bot]
2f17e4bb29 chore(deps): bump reqwest from 0.11.24 to 0.11.25
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.24 to 0.11.25.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.24...v0.11.25)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:24:32 -07:00
dependabot[bot]
1b966d3731 chore(deps): bump ctrlc from 3.4.2 to 3.4.4
Bumps [ctrlc](https://github.com/Detegr/rust-ctrlc) from 3.4.2 to 3.4.4.
- [Release notes](https://github.com/Detegr/rust-ctrlc/releases)
- [Commits](https://github.com/Detegr/rust-ctrlc/compare/3.4.2...3.4.4)

---
updated-dependencies:
- dependency-name: ctrlc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:24:14 -07:00
dependabot[bot]
e58d776f81 chore(deps): bump sysinfo from 0.30.6 to 0.30.7
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.6 to 0.30.7.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.30.6...v0.30.7)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:24:00 -07:00
LGUG2Z
40e77fddfe chore(release): v0.1.22 2024-03-03 21:00:37 -08:00
LGUG2Z
9c8a50fe80 docs(mkdocs): updates for v0.1.22 2024-03-03 20:53:25 -08:00
LGUG2Z
41e9068fca chore(wm): add debug event info to ignored windows 2024-03-03 14:58:12 -08:00
LGUG2Z
3690f8ebd8 ci(github): add common cargo checks 2024-03-03 13:31:20 -08:00
LGUG2Z
7b24474ef2 fix(wm): prevent ghost active border on empty ws
This commit ensures that a "ghost border" will not be drawn when
switching to empty workspaces if active_window_border = true.
2024-03-02 19:30:50 -08:00
LGUG2Z
f675717844 ci(github): add goreleaser check to build 2024-03-02 15:23:06 -08:00
LGUG2Z
38b0418c3b fix(subscriptions): emit ws event on empty targets
This commit ensures that workspace change events get emitted even when
changing to workspaces with no window containers. Previously these were
failing due to early returns triggered when the workspace focused did
not have a focused container.
2024-03-02 11:15:20 -08:00
LGUG2Z
6781f34930 fix(wm): restore full mouse resize functionality
This commit fixes a regression that was most likely introduced in #678
which changed a bunch of stuff related to window and border dimension
calculation.

See commit 4e98d7d36d for more information
as the same approach to fix the behaviour there has been applied here.
2024-03-01 19:07:05 -08:00
Javier Portillo
4919872e1a fix(wm): adds special case for grid stacks to the right 2024-03-01 16:14:42 -08:00
LGUG2Z
d730c3c72d feat(config): support parsing json w/ comments
This commit switches out the serde_json crate with the
serde_json_lenient crate, forked by Google, which allows for JSON files
with comments to be parsed properly.

Users can set the format of komorebi.json to "jsonc" in their editors in
order to write // comments without being faced with lint errors.

The expected file extension remains the same (json). komorebi and
komorebic will not look for files with the "jsonc" file extension or any
other JSON-variant file extension.

resolve #693
2024-02-29 17:35:47 -08:00
LGUG2Z
4e98d7d36d fix(wm): restore drag-to-swap window functionality
This commit fixes a regression that was most likely introduced in #678
which changed a bunch of stuff related to window and border dimension
calculation.

While we could previously assume that the points resize.right and
resize.bottom would always be 0 if we were dealing with a move rather
than a resize, these two points now depend on the values of BORDER_WIDTH
and BORDER_OFFSET. The code has been updated to reflect this and
calculate this constant just-in-time.
2024-02-29 17:22:56 -08:00
eythaann
2bceff4edc feat(wm): add path variant to application identifiers
This commit add a new way to match applications
through its paths, allowing to match a bunch
of applications that share a parent folder.
2024-02-28 12:04:46 -08:00
dependabot[bot]
b32bce8713 chore(deps): bump miette from 5.10.0 to 7.1.0
Bumps [miette](https://github.com/zkat/miette) from 5.10.0 to 7.1.0.
- [Release notes](https://github.com/zkat/miette/releases)
- [Changelog](https://github.com/zkat/miette/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zkat/miette/compare/miette-derive-v5.10.0...miette-derive-v7.1.0)

---
updated-dependencies:
- dependency-name: miette
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-27 22:51:28 -08:00
dependabot[bot]
47af40cf9e chore(deps): bump hotwatch from 0.4.6 to 0.5.0
Bumps [hotwatch](https://github.com/francesca64/hotwatch) from 0.4.6 to 0.5.0.
- [Release notes](https://github.com/francesca64/hotwatch/releases)
- [Changelog](https://github.com/francesca64/hotwatch/blob/main/CHANGELOG.md)
- [Commits](https://github.com/francesca64/hotwatch/compare/v0.4.6...v0.5.0)

---
updated-dependencies:
- dependency-name: hotwatch
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-27 21:55:12 -08:00
LGUG2Z
2b9fbc2074 chore(deps): bump windows-rs from 0.52 to 0.54 2024-02-27 18:33:29 -08:00
LGUG2Z
1a8b6a7398 feat(client): introduce komorebi-client crate
This commit introduces the komorebi-client library crate for other Rust
applications to interact with a running instance of komorebi over Unix
Domain Sockets.

Currently the crate re-exports everything one might find in the
komorebi::State struct and everything that is publicly exposed in
komorebi-core.

Public types and methods are still lacking documentation, and this crate
should not be published on crates.io until this is no longer the case.
2024-02-26 18:18:40 -08:00
42 changed files with 1211 additions and 955 deletions

View File

@@ -73,6 +73,11 @@ jobs:
- name: Install the target
run: |
rustup target install ${{ matrix.target }}
- name: Run Cargo checks
run: |
cargo fmt --check
cargo check
cargo clippy
- name: Run a full build
run: |
cargo build --locked --release --target ${{ matrix.target }}
@@ -93,6 +98,11 @@ jobs:
target/${{ matrix.target }}/release/komorebic-no-console.pdb
target/wix/komorebi-*.msi
retention-days: 7
- name: Check GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: build --skip=validate --clean
# Release
- name: Generate changelog
@@ -106,12 +116,12 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v')
with:
version: latest
args: release --skip-validate --clean --release-notes=CHANGELOG.md
args: release --skip=validate --clean --release-notes=CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
- name: Add MSI to release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: "target/wix/komorebi-*.msi"

616
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,14 +11,15 @@ members = [
]
[workspace.dependencies]
windows-interface = { version = "0.52" }
windows-implement = { version = "0.52" }
windows-interface = { version = "0.53" }
windows-implement = { version = "0.53" }
dunce = "1"
dirs = "5"
color-eyre = "0.6"
serde_json = { package = "serde_json_lenient", version = "0.1" }
[workspace.dependencies.windows]
version = "0.52"
version = "0.54"
features = [
"implement",
"Win32_System_Com",

View File

@@ -1,7 +1,7 @@
# alt-focus-hack
```
Enable or disable a hack simulating ALT key presses to ensure focus changes succeed
DEPRECATED since v0.1.22
Usage: komorebic.exe alt-focus-hack <BOOLEAN_STATE>

View File

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

View File

@@ -1,7 +1,7 @@
# check
```
Output various important komorebi-related environment values
Check komorebi configuration and related files for common errors
Usage: komorebic.exe check

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

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

View File

@@ -1,10 +0,0 @@
# docgen
```
Usage: komorebic.exe docgen
Options:
-h, --help
Print help
```

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
# subscribe
# subscribe-pipe
```
Subscribe to komorebi events
Subscribe to komorebi events using a Named Pipe
Usage: komorebic.exe subscribe <NAMED_PIPE>
Usage: komorebic.exe subscribe-pipe <NAMED_PIPE>
Arguments:
<NAMED_PIPE>

View File

@@ -0,0 +1,16 @@
# subscribe-socket
```
Subscribe to komorebi events using a Unix Domain Socket
Usage: komorebic.exe subscribe-socket <SOCKET>
Arguments:
<SOCKET>
Name of the socket to send event notifications to
Options:
-h, --help
Print help
```

View File

@@ -1,9 +1,9 @@
# unsubscribe
# unsubscribe-pipe
```
Unsubscribe from komorebi events
Usage: komorebic.exe unsubscribe <NAMED_PIPE>
Usage: komorebic.exe unsubscribe-pipe <NAMED_PIPE>
Arguments:
<NAMED_PIPE>

View File

@@ -0,0 +1,16 @@
# unsubscribe-socket
```
Unsubscribe from komorebi events
Usage: komorebic.exe unsubscribe-socket <SOCKET>
Arguments:
<SOCKET>
Name of the socket to stop sending event notifications to
Options:
-h, --help
Print help
```

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

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

View File

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

View File

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

View File

@@ -2,13 +2,15 @@
If you would like to remove all gaps by default, both between windows
themselves, and between the monitor edges and the windows, you can set the
following two configuration options to `0` in the `komorebi.json` configuration
file.
following configuration options to `0` and `-1` in the `komorebi.json`
configuration file.
```json
{
"default_workspace_padding": 0,
"default_container_padding": 0
"default_container_padding": 0,
"border_width": 0,
"border_offset": -1
}
```

View File

@@ -16,6 +16,12 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart
```
With the example configurations downloaded, you can now start `komorebi` and `whkd.
```powershell
komorebic start --whkd
```
## komorebi.json
The example window manager configuration sets some sane defaults and provides
@@ -139,6 +145,19 @@ If you have an ultrawide monitor, I recommend using this layout.
+-----+-----------+-----+
```
### Grid
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
```
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
| | | | | | | | | | | | | | |
| | | | | | | | | | | | | +---+
+-----+-----+ | +---+---+ +---+---+---+ +---+---| |
| | | | | | | | | | | | | +---+
| | | | | | | | | | | | | | |
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
4 windows 5 windows 6 windows 7 windows
```
## whkdrc

View File

@@ -1,15 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.22/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
"default_workspace_padding": 20,
"default_container_padding": 20,
"border_padding": 8,
"border_offset": -1,
"active_window_border": false,
"active_window_border_colours": {
"single": { "r": 66, "g": 165, "b": 245 },
"stack": { "r": 256, "g": 165, "b": 66 },
"monocle": { "r": 255, "g": 51, "b": 153 }
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399"
},
"monitors": [
{

56
docs/release/v0-1-22.md Normal file
View File

@@ -0,0 +1,56 @@
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
## tl;dr
The way windows are sized and drawn has been improved to remove the need to manually specify and remove invisible
borders for applications that overflow them. If you use the active window border, the first time you launch `v0.1.22`
you may end up with a _huge_ border due to these changes.
`active_window_border_width` and `active_window_border_offset` have been renamed to `border_width` and `border_offset`
as they now also apply outside the context of the active window border.
```json
{
"active_window_border": true,
"border_width": 8,
"border_offset": -1
}
```
Users of the active window border should start from these settings and read the notes below before making further
adjustments.
## Changes to `active_window_border`, and window sizing:
- The border no longer creates a second drop-shadow around the active window
- Windows are now sized to fill the layout region entirely, ignoring window decorations such as drop shadows
- Border offset now starts exactly at the paint edge of the window on all sides
- Windows are sized such that the border offset and border width are taken into account
## Recommended patterns
### Gapless
- Disable "transparency effects" Personalization > Colors
- Set the following settings in `komorebi.json`:
```json
{
"default_workspace_padding": 0,
"default_container_padding": 0,
"border_offset": -1,
"border_width": 0
}
```
### 1px border
A 1px border is drawn around the window edge. Users may see a gap for a single pixel, if the system theme has a
transparent edge - this is the windows themed edge, and is not present for all applications.
```json
{
"border_offset": 0,
"border_width": 1
}
```

View File

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

View File

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

View File

@@ -8,7 +8,9 @@ use strum::EnumString;
use crate::ApplicationIdentifier;
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema)]
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ApplicationOptions {
@@ -50,6 +52,13 @@ impl ApplicationOptions {
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MatchingRule {
Simple(IdWithIdentifier),
Composite(Vec<IdWithIdentifier>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifier {
pub kind: ApplicationIdentifier,
@@ -95,14 +104,14 @@ pub struct ApplicationConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<ApplicationOptions>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
pub float_identifiers: Option<Vec<MatchingRule>>,
}
impl ApplicationConfiguration {
pub fn populate_default_matching_strategies(&mut self) {
if self.identifier.matching_strategy.is_none() {
match self.identifier.kind {
ApplicationIdentifier::Exe => {
ApplicationIdentifier::Exe | ApplicationIdentifier::Path => {
self.identifier.matching_strategy = Option::from(MatchingStrategy::Equals);
}
ApplicationIdentifier::Class | ApplicationIdentifier::Title => {}
@@ -181,19 +190,21 @@ impl ApplicationConfigurationGenerator {
}
if let Some(float_identifiers) = app.float_identifiers {
for float in float_identifiers {
let float_rule =
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
for matching_rule in float_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule =
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if let Some(comment) = float.comment {
lines.push(format!("# {comment}"));
};
// if let Some(comment) = float.comment {
// lines.push(format!("# {comment}"));
// };
lines.push(float_rule);
lines.push(float_rule);
}
}
}
}
@@ -230,21 +241,23 @@ impl ApplicationConfigurationGenerator {
}
if let Some(float_identifiers) = app.float_identifiers {
for float in float_identifiers {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
float.kind, float.id
);
for matching_rule in float_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
float.kind, float.id
);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if let Some(comment) = float.comment {
lines.push(format!("; {comment}"));
};
// if let Some(comment) = float.comment {
// lines.push(format!("; {comment}"));
// };
lines.push(float_rule);
lines.push(float_rule);
}
}
}
}

View File

@@ -223,6 +223,8 @@ pub enum ApplicationIdentifier {
Class,
#[serde(alias = "title")]
Title,
#[serde(alias = "path")]
Path,
}
#[derive(

View File

@@ -1,15 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.22/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
"default_workspace_padding": 20,
"default_container_padding": 20,
"border_padding": 8,
"border_offset": -1,
"active_window_border": false,
"active_window_border_colours": {
"single": { "r": 66, "g": 165, "b": 245 },
"stack": { "r": 256, "g": 165, "b": 66 },
"monocle": { "r": 255, "g": 51, "b": 153 }
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399"
},
"monitors": [
{

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.22-dev.0"
version = "0.1.23-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -22,18 +22,18 @@ ctrlc = "3"
dirs = { workspace = true }
getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
hotwatch = "0.4"
hotwatch = "0.5"
lazy_static = "1"
miow = "0.5"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.7"
os_info = "3.8"
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
paste = "1"
regex = "1"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_json = { workspace = true }
strum = { version = "0.26", features = ["derive"] }
sysinfo = "0.30"
tracing = "0.1"

View File

@@ -10,7 +10,6 @@ use interfaces::IServiceProvider;
use std::ffi::c_void;
use windows::core::ComInterface;
use windows::core::Interface;
use windows::Win32::Foundation::HWND;
use windows::Win32::System::Com::CoCreateInstance;

View File

@@ -46,6 +46,7 @@ pub use windows_api::*;
use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
@@ -66,57 +67,57 @@ 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 {
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
MatchingRule::Simple(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 TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> =
Arc::new(Mutex::new(vec![
IdWithIdentifier {
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("explorer.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("chrome.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("idea64.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("ApplicationFrameHost.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("steam.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}
})
]));
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
IdWithIdentifier {
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("idea64.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
}),
]));
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -126,25 +127,26 @@ lazy_static! {
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<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = 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 {
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: String::from("OPContainerClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: String::from("IHWindowClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}
})
]));
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<MatchingRule>>> = 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(),
@@ -192,7 +194,6 @@ lazy_static! {
static ref BORDER_RECT: Arc<Mutex<Rect>> =
Arc::new(Mutex::new(Rect::default()));
static ref BORDER_OFFSET: AtomicI32 = Default::default();
// Use app-specific titlebar removal options where possible
// eg. Windows Terminal, IntelliJ IDEA, Firefox
@@ -212,7 +213,9 @@ pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20);
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
pub const TRANSPARENCY_COLOUR: u32 = 0;
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);

View File

@@ -23,6 +23,7 @@ use uds_windows::UnixStream;
use komorebi_core::config_generation::ApplicationConfiguration;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
@@ -273,17 +274,19 @@ impl WindowManager {
let mut should_push = true;
for m in &*manage_identifiers {
if m.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(m) = m {
if m.id.eq(id) {
should_push = false;
}
}
}
if should_push {
manage_identifiers.push(IdWithIdentifier {
manage_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::FloatRule(identifier, ref id) => {
@@ -291,17 +294,19 @@ impl WindowManager {
let mut should_push = true;
for f in &*float_identifiers {
if f.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(f) = f {
if f.id.eq(id) {
should_push = false;
}
}
}
if should_push {
float_identifiers.push(IdWithIdentifier {
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
let offset = self.work_area_offset;
@@ -315,6 +320,11 @@ impl WindowManager {
{
for window in container.windows() {
match identifier {
ApplicationIdentifier::Path => {
if window.path()? == *id {
hwnds_to_purge.push((i, window.hwnd));
}
}
ApplicationIdentifier::Exe => {
if window.exe()? == *id {
hwnds_to_purge.push((i, window.hwnd));
@@ -1020,17 +1030,19 @@ impl WindowManager {
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(IdWithIdentifier {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::IdentifyObjectNameChangeApplication(identifier, ref id) => {
@@ -1038,34 +1050,38 @@ impl WindowManager {
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(IdWithIdentifier {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::IdentifyTrayApplication(identifier, 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 let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(IdWithIdentifier {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::IdentifyLayeredApplication(identifier, ref id) => {
@@ -1073,17 +1089,19 @@ impl WindowManager {
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(IdWithIdentifier {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::ManageFocusedWindow => {
@@ -1384,10 +1402,14 @@ impl WindowManager {
);
}
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
if stack != 0 && self.focused_container()?.windows().len() > 1 {
BORDER_COLOUR_CURRENT
.store(stack, Ordering::SeqCst);
// it is not acceptable to fail here; we need to be able to send the event to
// subscribers
if self.focused_container().is_ok() {
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
if stack != 0 && self.focused_container()?.windows().len() > 1 {
BORDER_COLOUR_CURRENT
.store(stack, Ordering::SeqCst);
}
}
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));

View File

@@ -28,6 +28,8 @@ use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HIDDEN;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DATA_DIR;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
@@ -145,7 +147,7 @@ impl WindowManager {
match event {
WindowManagerEvent::Raise(window) => {
window.raise();
window.focus(false)?;
self.has_pending_raise_op = false;
}
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
@@ -185,6 +187,7 @@ impl WindowManager {
let title = &window.title()?;
let exe_name = &window.exe()?;
let class = &window.class()?;
let path = &window.path()?;
// 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.
@@ -193,6 +196,7 @@ impl WindowManager {
title,
exe_name,
class,
path,
&tray_and_multi_window_identifiers,
&regex_identifiers,
);
@@ -376,7 +380,14 @@ impl WindowManager {
// If we have moved across the monitors, use that override, otherwise determine
// if a move has taken place by ruling out a resize
let is_move = moved_across_monitors || resize.right == 0 && resize.bottom == 0;
let right_bottom_constant = ((BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst))
* 2)
.abs();
let is_move = moved_across_monitors
|| resize.right.abs() == right_bottom_constant
&& resize.bottom.abs() == right_bottom_constant;
if is_move {
tracing::info!("moving with mouse");
@@ -468,17 +479,17 @@ impl WindowManager {
let mut ops = vec![];
macro_rules! resize_op {
($coordinate:expr, $comparator:tt, $direction:expr) => {{
let adjusted = $coordinate * 2;
let sizing = if adjusted $comparator 0 {
Sizing::Decrease
} else {
Sizing::Increase
};
($coordinate:expr, $comparator:tt, $direction:expr) => {{
let adjusted = $coordinate * 2;
let sizing = if adjusted $comparator 0 {
Sizing::Decrease
} else {
Sizing::Increase
};
($direction, sizing, adjusted.abs())
}};
}
($direction, sizing, adjusted.abs())
}};
}
if resize.left != 0 {
ops.push(resize_op!(resize.left, >, OperationDirection::Left));
@@ -488,11 +499,14 @@ impl WindowManager {
ops.push(resize_op!(resize.top, >, OperationDirection::Up));
}
if resize.right != 0 && resize.left == 0 {
let top_left_constant = BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst);
if resize.right != 0 && resize.left == top_left_constant {
ops.push(resize_op!(resize.right, <, OperationDirection::Right));
}
if resize.bottom != 0 && resize.top == 0 {
if resize.bottom != 0 && resize.top == top_left_constant {
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
}

View File

@@ -32,11 +32,13 @@ use crate::WORKSPACE_RULES;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::notify::DebouncedEvent;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_core::config_generation::ApplicationConfiguration;
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
use komorebi_core::config_generation::ApplicationOptions;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::resolve_home_path;
use komorebi_core::ApplicationIdentifier;
@@ -269,22 +271,22 @@ pub struct StaticConfig {
pub global_work_area_offset: Option<Rect>,
/// Individual window floating rules
#[serde(skip_serializing_if = "Option::is_none")]
pub float_rules: Option<Vec<IdWithIdentifier>>,
pub float_rules: Option<Vec<MatchingRule>>,
/// Individual window force-manage rules
#[serde(skip_serializing_if = "Option::is_none")]
pub manage_rules: Option<Vec<IdWithIdentifier>>,
pub manage_rules: Option<Vec<MatchingRule>>,
/// Identify border overflow applications
#[serde(skip_serializing_if = "Option::is_none")]
pub border_overflow_applications: Option<Vec<IdWithIdentifier>>,
pub border_overflow_applications: Option<Vec<MatchingRule>>,
/// Identify tray and multi-window applications
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_and_multi_window_applications: Option<Vec<IdWithIdentifier>>,
pub tray_and_multi_window_applications: Option<Vec<MatchingRule>>,
/// Identify applications that have the WS_EX_LAYERED extended window style
#[serde(skip_serializing_if = "Option::is_none")]
pub layered_applications: Option<Vec<IdWithIdentifier>>,
pub layered_applications: Option<Vec<MatchingRule>>,
/// 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>>,
pub object_name_change_applications: Option<Vec<MatchingRule>>,
/// Set monitor index preferences
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
@@ -452,106 +454,40 @@ impl StaticConfig {
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 {
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 let Some(rules) = &mut self.float_rules {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
}
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(rules) = &mut self.manage_rules {
populate_rules(rules, &mut manage_identifiers, &mut regex_identifiers)?;
}
if let Some(identifiers) = &mut 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 let Some(rules) = &mut self.object_name_change_applications {
populate_rules(
rules,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(identifiers) = &mut 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 let Some(rules) = &mut self.layered_applications {
populate_rules(rules, &mut layered_identifiers, &mut regex_identifiers)?;
}
if let Some(identifiers) = &mut 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 let Some(rules) = &mut self.border_overflow_applications {
populate_rules(
rules,
&mut border_overflow_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(identifiers) = &mut 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 let Some(rules) = &mut self.tray_and_multi_window_applications {
populate_rules(
rules,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(path) = &self.app_specific_configuration_path {
@@ -560,120 +496,48 @@ impl StaticConfig {
let asc = ApplicationConfigurationGenerator::load(&content)?;
for mut 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 let Some(rules) = &mut entry.float_identifiers {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
}
if let Some(options) = entry.options {
if let Some(ref options) = entry.options {
let options = options.clone();
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);
}
}
populate_option(
&mut entry,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
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);
}
}
populate_option(
&mut entry,
&mut layered_identifiers,
&mut regex_identifiers,
)?;
}
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);
}
}
populate_option(
&mut entry,
&mut border_overflow_identifiers,
&mut regex_identifiers,
)?;
}
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) {
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);
}
}
populate_option(
&mut entry,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
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);
}
}
populate_option(
&mut entry,
&mut manage_identifiers,
&mut regex_identifiers,
)?;
}
}
}
@@ -744,10 +608,10 @@ impl StaticConfig {
let bytes = SocketMessage::ReloadStaticConfiguration(path.clone()).as_bytes()?;
wm.hotwatch.watch(path, move |event| match event {
wm.hotwatch.watch(path, move |event| match event.kind {
// 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(_) => {
EventKind::Modify(_) | EventKind::Remove(_) => {
let socket = DATA_DIR.join("komorebi.sock");
let mut stream =
UnixStream::connect(socket).expect("could not connect to komorebi.sock");
@@ -901,3 +765,67 @@ impl StaticConfig {
Ok(())
}
}
fn populate_option(
entry: &mut ApplicationConfiguration,
identifiers: &mut Vec<MatchingRule>,
regex_identifiers: &mut HashMap<String, Regex>,
) -> Result<()> {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
let rule = MatchingRule::Simple(entry.identifier.clone());
if !identifiers.contains(&rule) {
identifiers.push(rule);
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
}
Ok(())
}
fn populate_rules(
matching_rules: &mut Vec<MatchingRule>,
identifiers: &mut Vec<MatchingRule>,
regex_identifiers: &mut HashMap<String, Regex>,
) -> Result<()> {
for matching_rule in matching_rules {
if !identifiers.contains(matching_rule) {
match matching_rule {
MatchingRule::Simple(simple) => {
if simple.matching_strategy.is_none() {
simple.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if matches!(simple.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&simple.id)?;
regex_identifiers.insert(simple.id.clone(), re);
}
}
MatchingRule::Composite(composite) => {
for rule in composite {
if rule.matching_strategy.is_none() {
rule.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if matches!(rule.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&rule.id)?;
regex_identifiers.insert(rule.id.clone(), re);
}
}
}
}
identifiers.push(matching_rule.clone());
}
}
Ok(())
}

View File

@@ -4,11 +4,13 @@ use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Write as _;
use std::time::Duration;
use color_eyre::eyre;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use regex::Regex;
use schemars::JsonSchema;
@@ -215,57 +217,6 @@ impl Window {
WindowsApi::unmaximize_window(self.hwnd());
}
pub fn raise(self) {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
let current_thread_id = WindowsApi::current_thread_id();
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
// hook has been installed
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not attach to window thread input processing mechanism, but continuing execution of raise(): {}",
error
);
}
};
// Raise Window to foreground
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not set as foreground window, but continuing execution of raise(): {}",
error
);
}
};
// This isn't really needed when the above command works as expected via AHK
match WindowsApi::set_focus(self.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not set focus, but continuing execution of raise(): {}",
error
);
}
};
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, false) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not detach from window thread input processing mechanism, but continuing execution of raise(): {}",
error
);
}
};
}
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
@@ -377,6 +328,14 @@ impl Window {
WindowsApi::window_text_w(self.hwnd())
}
pub fn path(self) -> Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
let handle = WindowsApi::process_handle(process_id)?;
let path = WindowsApi::exe_path(handle);
WindowsApi::close_process(handle)?;
path
}
pub fn exe(self) -> Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
let handle = WindowsApi::process_handle(process_id)?;
@@ -440,8 +399,8 @@ impl Window {
(true, _) |
// If not allowing cloaked windows, we need to ensure the window is not cloaked
(false, false) => {
if let (Ok(title), Ok(exe_name), Ok(class)) = (self.title(), self.exe(), self.class()) {
return Ok(window_is_eligible(&title, &exe_name, &class, &self.style()?, &self.ex_style()?, event));
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (self.title(), self.exe(), self.class(), self.path()) {
return Ok(window_is_eligible(&title, &exe_name, &class, &path, &self.style()?, &self.ex_style()?, event));
}
}
_ => {}
@@ -455,6 +414,7 @@ fn window_is_eligible(
title: &String,
exe_name: &String,
class: &String,
path: &str,
style: &WindowStyle,
ex_style: &ExtendedWindowStyle,
event: Option<WindowManagerEvent>,
@@ -473,6 +433,7 @@ fn window_is_eligible(
title,
exe_name,
class,
path,
&float_identifiers,
&regex_identifiers,
);
@@ -482,6 +443,7 @@ fn window_is_eligible(
title,
exe_name,
class,
path,
&manage_identifiers,
&regex_identifiers,
);
@@ -495,6 +457,7 @@ fn window_is_eligible(
title,
exe_name,
class,
path,
&layered_whitelist,
&regex_identifiers,
);
@@ -512,6 +475,10 @@ fn window_is_eligible(
titlebars_removed.contains(exe_name)
};
if exe_name.contains("firefox") {
std::thread::sleep(Duration::from_millis(10));
}
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
// Get a lot of dupe events coming through that make the redrawing go crazy
@@ -522,8 +489,13 @@ fn window_is_eligible(
|| managed_override
{
return true;
} else if event.is_some() {
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
} else if let Some(event) = event {
tracing::debug!(
"ignoring (exe: {}, title: {}, event: {})",
exe_name,
title,
event
);
}
false
@@ -534,125 +506,204 @@ pub fn should_act(
title: &str,
exe_name: &str,
class: &str,
identifiers: &[IdWithIdentifier],
path: &str,
identifiers: &[MatchingRule],
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");
match identifier {
MatchingRule::Simple(identifier) => {
if should_act_individual(
title,
exe_name,
class,
path,
identifier,
regex_identifiers,
) {
should_act = true
};
}
Some(MatchingStrategy::Legacy) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
should_act = true;
}
MatchingRule::Composite(identifiers) => {
let mut composite_results = vec![];
for identifier in identifiers {
composite_results.push(should_act_individual(
title,
exe_name,
class,
path,
identifier,
regex_identifiers,
));
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
should_act = true;
}
if composite_results.iter().all(|&x| x) {
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
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
pub fn should_act_individual(
title: &str,
exe_name: &str,
class: &str,
path: &str,
identifier: &IdWithIdentifier,
regex_identifiers: &HashMap<String, Regex>,
) -> bool {
let mut should_act = false;
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;
}
}
ApplicationIdentifier::Path => {
if path.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;
}
}
ApplicationIdentifier::Path => {
if path.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;
}
}
ApplicationIdentifier::Path => {
if path.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;
}
}
ApplicationIdentifier::Path => {
if path.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;
}
}
ApplicationIdentifier::Path => {
if path.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;
}
}
}
ApplicationIdentifier::Path => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(path) {
should_act = true;
}
}
}
},
}
should_act
}

View File

@@ -12,7 +12,8 @@ use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::notify::DebouncedEvent;
use hotwatch::notify::ErrorKind as NotifyErrorKind;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use parking_lot::Mutex;
use schemars::JsonSchema;
@@ -20,7 +21,7 @@ use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixListener;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::custom_layout::CustomLayout;
use komorebi_core::Arrangement;
use komorebi_core::Axis;
@@ -96,12 +97,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 float_identifiers: Vec<MatchingRule>,
pub manage_identifiers: Vec<MatchingRule>,
pub layered_whitelist: Vec<MatchingRule>,
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
pub border_overflow_identifiers: Vec<MatchingRule>,
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
pub monitor_index_preferences: HashMap<usize, Rect>,
pub display_index_preferences: HashMap<usize, String>,
}
@@ -211,12 +212,16 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn show_border(&self) -> Result<()> {
let foreground = WindowsApi::foreground_window()?;
let foreground_window = Window { hwnd: foreground };
if self.focused_container().is_ok() {
let foreground = WindowsApi::foreground_window()?;
let foreground_window = Window { hwnd: foreground };
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.set_position(foreground_window, true)?;
WindowsApi::invalidate_border_rect()
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.set_position(foreground_window, true)?;
WindowsApi::invalidate_border_rect()?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
@@ -261,18 +266,18 @@ impl WindowManager {
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()),
hotwatch::Error::Notify(ref notify_error) => match notify_error.kind {
NotifyErrorKind::WatchNotFound => {}
_ => return Err(error.into()),
},
error @ hotwatch::Error::Io(_) => return Err(error.into()),
},
}
self.hotwatch.watch(config, |event| match event {
self.hotwatch.watch(config, |event| match event.kind {
// 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(_) => {
EventKind::Modify(_) | EventKind::Remove(_) => {
std::thread::spawn(|| {
load_configuration().expect("could not load configuration");
});
@@ -666,63 +671,44 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> {
let mut hwnd = WindowsApi::window_at_cursor_pos()?;
let mut hwnd = None;
if self.has_pending_raise_op
|| self.focused_window()?.hwnd == hwnd
// Sometimes we need this check, because the focus may have been given by a click
// to a non-window such as the taskbar or system tray, and komorebi doesn't know that
// the focused window of the workspace is not actually focused by the OS at that point
|| WindowsApi::foreground_window()? == hwnd
{
Ok(())
} else {
let mut known_hwnd = false;
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
if workspace.contains_window(hwnd) {
known_hwnd = true;
}
}
}
// TODO: Not sure if this needs to be made configurable just yet...
let overlay_classes = [
// Chromium/Electron
"Chrome_RenderWidgetHostHWND".to_string(),
// Explorer
"DirectUIHWND".to_string(),
"SysTreeView32".to_string(),
"ToolbarWindow32".to_string(),
"NetUIHWND".to_string(),
];
if !known_hwnd {
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 { hwnd }.exe()?)
{
hwnd = exe_hwnd;
known_hwnd = true;
}
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
if let Some(container_idx) = workspace.container_idx_from_current_point() {
if let Some(container) = workspace.containers().get(container_idx) {
if let Some(window) = container.focused_window() {
hwnd = Some(window.hwnd);
}
}
}
}
if known_hwnd {
let event = WindowManagerEvent::Raise(Window { hwnd });
self.has_pending_raise_op = true;
Ok(winevent_listener::event_tx().send(event)?)
} else {
tracing::debug!("not raising unknown window: {}", Window { hwnd });
Ok(())
}
}
if let Some(hwnd) = hwnd {
if self.has_pending_raise_op
|| self.focused_window()?.hwnd == hwnd
// Sometimes we need this check, because the focus may have been given by a click
// to a non-window such as the taskbar or system tray, and komorebi doesn't know that
// the focused window of the workspace is not actually focused by the OS at that point
|| WindowsApi::foreground_window()? == hwnd
{
return Ok(());
}
let event = WindowManagerEvent::Raise(Window { hwnd });
self.has_pending_raise_op = true;
winevent_listener::event_tx().send(event)?;
} else {
tracing::debug!(
"not raising unknown window: {}",
Window {
hwnd: WindowsApi::window_at_cursor_pos()?
}
);
}
Ok(())
}
#[tracing::instrument(skip(self))]
@@ -851,17 +837,18 @@ impl WindowManager {
}
}
// if we passed false for follow_focus
if !follow_focus
// if we passed false for follow_focus and there is a container on the workspace
if !follow_focus && self.focused_container_mut().is_ok() {
// and we have a stack with >1 windows
&& self.focused_container_mut()?.windows().len() > 1
if self.focused_container_mut()?.windows().len() > 1
// and we don't have a maxed window
&& self.focused_workspace()?.maximized_window().is_none()
// and we don't have a monocle container
&& self.focused_workspace()?.monocle_container().is_none()
{
if let Ok(window) = self.focused_window_mut() {
window.focus(self.mouse_follows_focus)?;
{
if let Ok(window) = self.focused_window_mut() {
window.focus(self.mouse_follows_focus)?;
}
}
};
@@ -1449,7 +1436,9 @@ impl WindowManager {
anyhow!("this is not a valid direction from the current position")
})?;
let adjusted_new_index = if new_idx > current_container_idx {
let adjusted_new_index = if new_idx > current_container_idx
&& !matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid))
{
new_idx - 1
} else {
new_idx

View File

@@ -139,11 +139,13 @@ impl WindowManagerEvent {
let title = &window.title().ok()?;
let exe_name = &window.exe().ok()?;
let class = &window.class().ok()?;
let path = &window.path().ok()?;
let should_trigger = should_act(
title,
exe_name,
class,
path,
&object_name_change_on_launch,
&regex_identifiers,
);

View File

@@ -357,7 +357,7 @@ impl WindowsApi {
bottom: layout.bottom + shadow_rect.bottom,
};
let position = if top { HWND_TOP } else { HWND_BOTTOM };
let position = if top { HWND_TOP } else { HWND_NOTOPMOST };
Self::set_window_pos(hwnd, &rect, position, flags.bits())
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.22-dev.0"
version = "0.1.23-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]
@@ -21,12 +21,12 @@ dunce = { workspace = true }
fs-tail = "0.1"
heck = "0.4"
lazy_static = "1"
miette = { version = "5", features = ["fancy"] }
miette = { version = "7", features = ["fancy"] }
paste = "1"
powershell_script = "1.0"
reqwest = { version = "0.11", features = ["blocking"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_json = { workspace = true }
serde_yaml = "0.9"
sysinfo = "0.30"
thiserror = "1"

View File

@@ -114,7 +114,7 @@ trait AhkFunction {
struct ConfigurationError {
message: String,
#[source_code]
src: NamedSource,
src: NamedSource<String>,
#[label("This bit here")]
bad_bit: SourceSpan,
}
@@ -1367,7 +1367,7 @@ fn main() -> Result<()> {
let diagnostic = ConfigurationError {
message: msgs[0].to_string(),
src: NamedSource::new("komorebi.json", config_source.clone()),
bad_bit: SourceSpan::new(offset, 2.into()),
bad_bit: SourceSpan::new(offset, 2),
};
println!("{:?}", Report::new(diagnostic));

View File

@@ -62,17 +62,23 @@ nav:
- common-workflows/custom-layouts.md
- common-workflows/dynamic-layout-switching.md
# - common-workflows/autohotkey.md
- Release notes:
- release/v0-1-22.md
- Configuration reference: https://komorebi.lgug2z.com/schema
- CLI reference:
- cli/quickstart.md
- cli/start.md
- cli/stop.md
- cli/check.md
- cli/configuration.md
- cli/whkdrc.md
- cli/state.md
- cli/visible-windows.md
- cli/query.md
- cli/subscribe.md
- cli/unsubscribe.md
- cli/subscribe-socket.md
- cli/unsubscribe-socket.md
- cli/subscribe-pipe.md
- cli/unsubscribe-pipe.md
- cli/log.md
- cli/quick-save-resize.md
- cli/quick-load-resize.md
@@ -160,7 +166,6 @@ nav:
- cli/reload-configuration.md
- cli/watch-configuration.md
- cli/complete-configuration.md
- cli/alt-focus-hack.md
- cli/window-hiding-behaviour.md
- cli/cross-monitor-move-behaviour.md
- cli/toggle-cross-monitor-move-behaviour.md

View File

@@ -44,7 +44,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"ApplicationOptions": {

View File

@@ -133,20 +133,15 @@
}
}
},
"active_window_border_offset": {
"description": "Offset of the active window border (default: -1)",
"type": "integer",
"format": "int32"
},
"active_window_border_width": {
"description": "Width of the active window border (default: 8)",
"type": "integer",
"format": "int32"
},
"app_specific_configuration_path": {
"description": "Path to applications.yaml from komorebi-application-specific-configurations (default: None)",
"type": "string"
},
"border_offset": {
"description": "Offset of the window border (default: -1)",
"type": "integer",
"format": "int32"
},
"border_overflow_applications": {
"description": "Identify border overflow applications",
"type": "array",
@@ -165,7 +160,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -182,6 +178,11 @@
}
}
},
"border_width": {
"description": "Width of the window border (default: 8)",
"type": "integer",
"format": "int32"
},
"cross_monitor_move_behaviour": {
"description": "Determine what happens when a window is moved across a monitor boundary (default: Swap)",
"oneOf": [
@@ -236,7 +237,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -305,7 +307,7 @@
}
},
"invisible_borders": {
"description": "Dimensions of Windows' own invisible borders; don't set these yourself unless you are told to",
"description": "DEPRECATED from v0.1.22: no longer required",
"type": "object",
"required": [
"bottom",
@@ -354,7 +356,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -389,7 +392,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -525,7 +529,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -551,7 +556,8 @@
"Rows",
"VerticalStack",
"HorizontalStack",
"UltrawideVerticalStack"
"UltrawideVerticalStack",
"Grid"
]
},
"layout_rules": {
@@ -565,7 +571,8 @@
"Rows",
"VerticalStack",
"HorizontalStack",
"UltrawideVerticalStack"
"UltrawideVerticalStack",
"Grid"
]
}
},
@@ -596,7 +603,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -641,7 +649,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -663,6 +672,143 @@
"type": "integer",
"format": "int32"
},
"stackbar": {
"type": "object",
"properties": {
"height": {
"type": "integer",
"format": "int32"
},
"mode": {
"type": "string",
"enum": [
"Always",
"Never",
"OnStack"
]
},
"tabs": {
"type": "object",
"properties": {
"background": {
"anyOf": [
{
"description": "Colour represented as RGB",
"type": "object",
"required": [
"b",
"g",
"r"
],
"properties": {
"b": {
"description": "Blue",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"g": {
"description": "Green",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"r": {
"description": "Red",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
}
},
{
"description": "Colour represented as Hex",
"type": "string"
}
]
},
"focused_text": {
"anyOf": [
{
"description": "Colour represented as RGB",
"type": "object",
"required": [
"b",
"g",
"r"
],
"properties": {
"b": {
"description": "Blue",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"g": {
"description": "Green",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"r": {
"description": "Red",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
}
},
{
"description": "Colour represented as Hex",
"type": "string"
}
]
},
"unfocused_text": {
"anyOf": [
{
"description": "Colour represented as RGB",
"type": "object",
"required": [
"b",
"g",
"r"
],
"properties": {
"b": {
"description": "Blue",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"g": {
"description": "Green",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"r": {
"description": "Red",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
}
},
{
"description": "Colour represented as Hex",
"type": "string"
}
]
},
"width": {
"type": "integer",
"format": "int32"
}
}
}
}
},
"tray_and_multi_window_applications": {
"description": "Identify tray and multi-window applications",
"type": "array",
@@ -681,7 +827,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {