Compare commits

..

80 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
James Tucker
e5cf042ea9 fix(komorebi): fix unsound use of transmute
This was causing a crash in release mode when moving a window around by
titlebar.
2024-02-26 11:00:34 -08:00
James Tucker
c435f84afc fix(komorebi): remove some warnings that just snuck in 2024-02-25 22:31:39 -08:00
LGUG2Z
de0db4d014 docs(readme): contributing, uds subs + rust client
This commit adds some contribution guidelines and updates the "Window
Manager Event Subscriptions" section with information on using
subscribe-socket and komorebi-client.
2024-02-25 21:53:11 -08:00
James Tucker
0afcf6d86a fix(komorebi): don't scale for DPI, as we're not DPI aware
This fixes a regression from 344e6ad2fd
that assumed we were declared DPI aware.
2024-02-25 19:01:48 -08:00
LGUG2Z
e0e3afa5b9 feat(config): update border opts, add deprecations
This commit includes backwards-compatible renames of
active_window_border_offset and active_window_border_width to
border_offset and border_width respectively.

Since the invisible window border adjustments were removed in #678, the
invisible window borders set by the OS can now also be adjusted via
border_offset and border_width, which is the primary reason for the
rename.

invisible_borders has been marked as deprecated and the previously
deprecated alt_focus_hack option has been removed.
2024-02-25 17:41:49 -08:00
James Tucker
94d8f72904 fix(komorebi): don't raise the border window to top
This trades one issue for another, in order of importance:

- Pop-up windows such as a file upload dialog box for Firefox no longer
  have a window border drawn over the top - better.
- Opaquely bordered windows without DWM decorations, combined with a -1
  offset / single pixel border end up as invisible borders (e.g. EPIC
  Games Launcher).
2024-02-25 16:27:16 -08:00
James Tucker
9b9777feaf fix(komorebi): close the corner gap around rounded corners
The default window corner rounding is still slightly visible at offset
-1 until this corner radius that completely closes up the transparent
region, without needing to invasively draw over the target window.
2024-02-25 16:27:16 -08:00
James Tucker
0c2e37e127 fix(komorebi): raise windows and border to top, not topmost
topmost has a special meaning, in that it is a sticky raise, which is
not what we want - we don't want to be permanently above all other
windows, as this leads to bugs where for example the windows can end up
stuck above non-topmost windows in a different security context, such as
programs running with administrator priviliges.
2024-02-25 16:27:16 -08:00
James Tucker
10ae60f79b fix(config): set new default border offset and width
In the new border painting strategy, the 20 pixel border is huge. The
border is now offset -1 by default, so as to draw over the standard DWM
1px border (avoiding a "1 pixel see through gap"), and the default width
is 8px.
2024-02-25 16:27:16 -08:00
James Tucker
fbb34ba4b3 fix(komorebi): account for border decorations on resize
The SetWindowPos API will inset the provided dimensions by the amount of
space required for window decorations (shadows, etc). Compute the size
of the decorations and add them as padding to the provided size,
resulting in windows being set precisely to the target dimensions.

An active_window_border_width=1, active_window_border_offset=-1 will now
paint over the 1px window decoration consistently.

The border window is always made topmost on resize, so that it paints
over custom borders (e.g. EPIC Games Launcher) as those borders are
opaque and as such a 1px border configuration as above becomes invisible
in that condition.
2024-02-25 16:27:16 -08:00
James Tucker
5ee827ecaf fix(komorebi): account for border offset and width in layout
The layout should leave the space configured for the border, so that the
border always stays within the workspace bounds.

Border offset is cleaned up, as it is no longer a rect, but instead just
a fixed value.

The rect function for adjusting padding now takes a concrete value, as
the optional has no local meaning to the operation, being equivalent to
the default value.

A margin function is added to centralize the notion of increasing the
size of a rect by the given margin, the opposite of the padding
operation.
2024-02-25 16:27:16 -08:00
James Tucker
dc3ffb3bcb fix(komorebi): restore borders, no more DWM border on border window
This fixes a regression from an earlier commit that dropped the DWM
style borders without fixing corner rounding on Windows 11. This is now
fixed for Windows 11 again, while avoiding the extra system border
decorations.
2024-02-25 16:27:16 -08:00
James Tucker
4affefad88 fix(cfg,komorebi): remove all uses of invisible borders
This makes the borders pixel-perfect, and border_overflow can be
disabled on all applications.

Unfortunately this also means we lose the corner rounding, so that may
need to be done differently or the offsets refined in some way to
address that.
2024-02-25 16:27:16 -08:00
James Tucker
344e6ad2fd komorebi{,-core}: use window bounds without shadow
Switch to using the DWM API to get Window bounds so as to exclude the
outside of window decorations from the computation.

This is getting close to a precise window size, you can now set an
active border width of 1 and an offset of 1 and get a 1 pixel line
around most windows, except that there's some extra top padding I have
yet to find the cause of.

This implementation needs to be DPI aware, but I haven't yet tested if
the DPI scaling approach is entirely valid - we may instead need to get
the per-monitor DPI scale, identify the monitor the window is on, and
scale to that, rather than using the system wide scale.

Maybe fixes #574
Maybe updates #622
2024-02-25 16:27:16 -08:00
LGUG2Z
fd57d32bb5 refactor(clippy): apply various lint fixes and recs 2024-02-25 13:33:44 -08:00
LGUG2Z
0581950b21 feat(cli): add whkdrc config path command
This commit adds the "komorebic whkdrc" command to go along with the
"komorebic configuration" command introduced in
608ec03047, aimed it making it easier to
edit this file using a command line editor.

The "config" command has been renamed in the code to Configuration, with
an alias of "config". The Whkdrc command similarly comes with the "whkd"
alias.
2024-02-25 13:26:05 -08:00
LGUG2Z
a07bb4ac60 docs(mkdocs): update old videos in common workflows 2024-02-25 13:06:24 -08:00
Javier Portillo
eab7a64250 fix(grid): enables flip_layout and make it behave correctly 2024-02-25 11:12:34 -08:00
Javier Portillo
98244b9572 feat(wm): passes optional op_direction and count to <dir>_index functions 2024-02-25 11:12:34 -08:00
Javier Portillo
2c156e9a99 refactor(grid): use matches! for early returns 2024-02-25 11:12:34 -08:00
Javier Portillo
d33df04f38 fix(grid): prevents axis flips on grid layout 2024-02-25 11:12:34 -08:00
Javier Portillo
9c196b99c9 feat(grid): adds no-operations for Promote and PromoteFocus commands 2024-02-25 11:12:34 -08:00
Javier Portillo
9fcf4ec19f feat(wm): add grid layout
Based on the Grid layout on LeftWM

Co-authored-by: LGUG2Z <LGUG2Z@fastmail.com>
2024-02-25 11:12:34 -08:00
dependabot[bot]
c3e39311c1 chore(deps): bump which from 5.0.0 to 6.0.0
Bumps [which](https://github.com/harryfei/which-rs) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/harryfei/which-rs/releases)
- [Changelog](https://github.com/harryfei/which-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harryfei/which-rs/compare/5.0.0...6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-25 11:02:19 -08:00
LGUG2Z
e7d928a065 feat(config): allow colours in both rgb and hex
This commit introduces the ability for users to specify colours in the
static config file in either RGB or Hex format.

This is done by introducing a new enum, Colour, which provides variants
wrapping the internal Rgb type, and the HexColour type from the
hex_colour crate. HexColour itself is wrapped in a new struct, Hex, in
order to allow the implementation of the JsonSchema trait.

A number of From<T> implementations have been done in order to make
working with the Colour enum more ergonomic.

Ultimately, the core representation for colours in komorebi remains the
Rgb struct, any and Hex-specified colours will be converted to this Rgb
type before being converted to u32 to pass them on to various Win32 API
calls.
2024-02-25 10:37:00 -08:00
LGUG2Z
608ec03047 feat(cli): add config command
This commit adds the "komorebic config" command as a helper to print the
path to komorebi.json, while respecting the KOMOREBI_CONFIG_HOME
environment variable if it exists.

This is particularly useful for users who wish to edit this file on the
command line, as they can now run commands like:

"lvim $(komorebic config)"
2024-02-25 09:31:57 -08:00
LGUG2Z
92359ebaed fix(wm): improve floating ws window handling
This commit addresses and number of bugs and improves the experience of
working with floating workspaces (ie. Workspace.tile = false).

- When the user moves or resizes a window on a floating workspace,
  WindowManagerEvent::MoveResizeStart will no longer trigger, which
  prevents the mouse focus from going to the middle of the window rect
  after the resize or move action (if mouse_follows_focus = true)

- If active_window_border = true, it will no longer be shown on focused
  windows in floating workspaces

- When windows are moved using a komorebic command from a floating
  workspace to a tiling workspace and active_window_border = true, the
  active window border will be shown again
2024-02-25 08:37:00 -08:00
LGUG2Z
c19f64144a fix(cli): remove socket connection retry loop
This commit remove the socket connection retry loop in send_message
which is no longer required after @raggi's changes in
c8f6502b02.

@azinsharaf noticed that when trying to run komorebic commands while
komorebi was not accepting connections, multiple hanging komorebic
instances could be spawned, particularly if commands were retried.

@eythaann proposed an additive fix for this in PR684 but ultimately as
the previous race condition with the query/response commands has been
handed by @raggi we can remove the socket connection retry loop
completely.
2024-02-25 08:17:20 -08:00
LGUG2Z
8642ac0946 fix(wm): improve maximized window handling
This commit addresses a few bugs with regards to maximized window
handling.

- Correctly restoring and focusing when switching to a workspace
  containing a window previously maximized with the toggle-maximize
  command

- Unmaximizing a window during the initial window scan when the wm
  initializes so that windows that are maximized before running komorebi
  will no longer have the ugly white bar at the top

- When updating workspace layouts, windows that have been maximized
  without using the komorebic toggle-maximize command will be
  unmaximized to prevent the wm state drifting out of sync with what is
  happening on the screen
2024-02-24 19:30:38 -08:00
LGUG2Z
dee5842c9c chore(deps): cargo update 2024-02-24 17:36:43 -08:00
LGUG2Z
a6deeef717 fix(wm): cycle stack focus w/ mff disabled
This commit ensures that the focus changes to the appropriate window
when a container stack is being cycled through by a user who has
disabled the mouse_follows_focus feature.

fix #680
2024-02-24 13:01:45 -08:00
James Tucker
0160e8eeeb fix(wm): cleanup window event messaging
- Use a single thread to bind the hook, and then start dispatching.
- Use a blocking loop for message dispatching.
- Remove the locks around crossbeam channel, as it's already Send + Sync
2024-02-20 18:01:56 -08:00
LGUG2Z
f519cbaf1e refactor(wm): split komorebi into bin and lib
This commit splits the komorebi crate into a mixed binary and library
crate.

All types and logic related to window management have been moved under
lib.rs, and are imported from there for use in main.rs, which is now
only responsible for starting and running the window manager process.

In preparation for exposing a new komorebi-client crate in the future,
serde::Deserialize has been derived on Notification and any struct that
may appear in a notification receievd by a process that has subscribed
to event updates.

re #659
2024-02-18 15:14:59 -08:00
LGUG2Z
ef1ce4a389 feat(subscriptions): add uds subscription support
This commit adds support for subscriptions via Unix Domain Sockets which
are better suited for IPC between Rust processes compared to Named Pipes
which have issues that I don't want to spend time resolving.

The main motivation for this change is to provide an easy way for the
new zebar project to consume information about komorebi's state in the
Rust backend so that a bar module can be created for komorebi users.

The next step in this process will be to finally refactor the komorebi
crate into a mixed bin/lib crate, and expose all notification-related
structs and maybe some connection helper methods in a new
komorebi-client crate.

The previous "subscribe" and "unsubscribe" komorebic commands have had
the "-pipe" suffix added to them, with aliases in place for the previous
names in order to ensure backwards compat.
2024-02-17 18:14:14 -08:00
James Tucker
c8f6502b02 fix(cli,tcp): replies are sent on the requesting channel
Replace the client socket with replies sent on the other side of the
querying stream, for both UDS and TCP clients. This has two results:
unix socket clients such as komorebic no longer race on the socket bind,
fixing the out of order bind error, and the response mixup conditions
that could occur. Queries over TCP now receive replies over TCP, rather
than replies being sent to a future or in-flight command line client.
2024-02-17 11:03:42 -08:00
LGUG2Z
afd93c34a2 docs(readme): add v0.1.21+ quickstart video 2024-02-16 15:14:15 -08:00
LGUG2Z
d52715a8fa fix(cli): create local appdata dir w/ quickstart
This commit ensures that the $Env:LOCALAPPDATA/komorebi dir is created
by the quickstart command, as it cannot be assumed that this will always
exist, especially on new machines with recent versions of Windows 11.

fix #671
2024-02-16 13:15:44 -08:00
LGUG2Z
549500887f chore(dev): begin v0.1.22-dev 2024-02-16 13:04:51 -08:00
LGUG2Z
40947e39e8 docs(quickstart): ensure $env:localappdata\komorebi creation 2024-02-16 08:51:42 -08:00
LGUG2Z
17a45804b4 chore(release): v0.1.21 2024-02-15 17:56:06 -08:00
LGUG2Z
0e14f25130 fix(scoop): detect shims correctly w/ sysinfo 0.30
This commit is a fix that handles a subtle breaking change in
sysinfo::Process:root() which no longer can be used to see if a process
is a scoop shim.

Instead we can stringify the executable path and see if the absolute exe
path contains the substring "shims".

With this fix duplicate process detection is once again working
correctly.
2024-02-15 17:54:03 -08:00
LGUG2Z
380971edee chore(dev): begin v0.1.21-dev 2024-02-15 14:05:49 -08:00
LGUG2Z
52122c401d chore(release): v0.1.20 2024-02-15 12:55:01 -08:00
LGUG2Z
e5ebf55115 docs(readme): add references to docs website 2024-02-15 12:21:51 -08:00
LGUG2Z
0c75ec37d0 docs(mkdocs): add common workflows section 2024-02-15 12:21:51 -08:00
LGUG2Z
731a4465f1 feat(config): reduce noise in jsonschema output 2024-02-15 12:21:51 -08:00
LGUG2Z
596884e9fd docs(mkdocs): link to ext jsonschema docgen 2024-02-15 12:21:51 -08:00
LGUG2Z
5ef53c2b68 docs(mkdocs): add cli reference 2024-02-15 12:21:51 -08:00
LGUG2Z
a00a85e63f feat(cli): add docgen cmd for mkdocs pages 2024-02-15 12:21:51 -08:00
LGUG2Z
e0aa0ac843 docs(mkdocs): add index and getting started sections 2024-02-15 12:21:51 -08:00
LGUG2Z
3e6e586d5b docs(mkdocs): start building dedicated site 2024-02-15 12:21:51 -08:00
62 changed files with 2968 additions and 2012 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 }}
@@ -87,10 +92,17 @@ jobs:
path: |
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebic-no-console.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
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
@@ -104,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"

View File

@@ -34,7 +34,7 @@ builds:
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"
- 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"

687
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,20 +4,22 @@ resolver = "2"
members = [
"derive-ahk",
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebic",
"komorebic-no-console",
]
[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",

202
README.md
View File

@@ -82,6 +82,8 @@ A [detailed installation and quickstart
guide](https://lgug2z.github.io/komorebi/installation.html) is available which shows how to get started
using `scoop`, `winget` or building from source.
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/H9-_c1egQ4g/hqdefault.jpg)](https://www.youtube.com/watch?v=H9-_c1egQ4g)
# Demonstrations
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
@@ -99,20 +101,76 @@ widget enabled. The original video can be viewed
https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-96fb-a8985380bc70.mp4
# Development
# Contribution Guidelines
If you would like to contribute code to this repository, there are a few requests that I have to ensure a foundation of
code quality, consistency and commit hygiene:
If you would like to contribute to `komorebi` please take the time to carefully read the guidelines below.
## Commit hygiene
- Flatten all `use` statements
- Run `cargo +nightly clippy` and ensure that all lints and suggestions have been addressed before committing
- Run `cargo +stable clippy` and ensure that all lints and suggestions have been addressed before committing
- Run `cargo +nightly fmt --all` to ensure consistent formatting before committing
- Use `git cz` with
the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare
commit messages
- Provide at least one short sentence or paragraph in your commit message body to describe your thought process for the
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the
changes being committed
## PRs should contain only a single feature or bug fix
It is very difficult to review pull requests which touch multiple unrelated features and parts of the codebase.
Please do not submit pull requests like this; you will be asked to separate them into smaller PRs that deal only with
one feature or bug fix at a time.
If you are working on multiple features and bug fixes, I suggest that you cut a branch called `local-trunk`
from `master` which you keep up to date, and rebase the various independent branches you are working on onto that branch
if you want to test them together or create a build with everything integrated.
## Refactors to the codebase must have prior approval
`komorebi` is a mature codebase with an internal consistency and structure that has developed organically over close to
half a decade.
There are [countless hours of live coding videos](https://youtube.com/@LGUG2Z) demonstrating work on this project and
showing new contributors how to do everything from basic tasks like implementing new `komorebic` commands to
distinguishing monitors by manufacturer hardware identifiers and video card ports.
Refactors to the structure of the codebase are not taken lightly and require prior discussion and approval.
Please do not start refactoring the codebase with the expectation of having your changes integrated until you receive an
explicit approval or a request to do so.
Similarly, when implementing features and bug fixes, please stick to the structure of the codebase as much as possible
and do not take this as an opportunity to do some "refactoring along the way".
It is extremely difficult to review PRs for features and bug fixes if they are lost in sweeping changes to the structure
of the codebase.
## Breaking changes to user-facing interfaces are unacceptable
This includes but is not limited to:
- All `komorebic` commands
- The `komorebi.json` schema
- The [`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
schema
No user should ever find that their configuration file has stopped working after upgrading to a new version
of `komorebi`.
More often than not there are ways to reformulate changes that may initially seem like they require breaking user-facing
interfaces into additive changes.
For some inspiration please take a look
at [this commit](https://github.com/LGUG2Z/komorebi/commit/e7d928a065eb63bb4ea1fb864c69c1cae8cc763b) which added the
ability for users to specify colours in `komorebi.json` in Hex format alongside RGB.
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
required.
# Development
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
the IDE for completions and navigation:
@@ -151,20 +209,21 @@ found, information about it will appear in the log which can be shared when open
# Window Manager State and Integrations
The current state of the window manager can be queried using the `komorebic state` command, which returns a JSON
representation of the `State` struct, which includes the current state of `WindowManager`.
representation of the `State` struct.
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
This may also be polled to build further integrations and widgets on top of.
# Window Manager Event Subscriptions
It is also possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
## Named Pipes
It is possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
by `komorebi` using [Named Pipes](https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes).
First, your application must create a named pipe. Once the named pipe has been created, run the following command:
```powershell
komorebic.exe subscribe <your pipe name>
komorebic.exe subscribe-pipe <your pipe name>
```
Note that you do not have to include the full path of the named pipe, just the name.
@@ -188,12 +247,125 @@ You may then filter on the `type` key to listen to the events that you are inter
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
in `komorebi-core`.
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Python
by [@denBot](https://github.com/denBot) can be
found [here](https://gist.github.com/denBot/4136279812f87819f86d99eba77c1ee0).
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Rust can also be found
in the [`komokana`](https://github.com/LGUG2Z/komokana) repository.
```javascript
const { exec } = require("child_process");
const net = require("net");
const pipeName = "\\\\.\\pipe\\komorebi-js";
const server = net.createServer((stream) => {
console.log("Client connected");
// Every time there is a workspace-related event, let's log the names of all
// workspaces on the currently focused monitor, and then log the name of the
// currently focused workspace on that monitor
stream.on("data", (data) => {
let json = JSON.parse(data.toString());
let event = json.event;
if (event.type.includes("Workspace")) {
let monitors = json.state.monitors;
let current_monitor = monitors.elements[monitors.focused];
let workspaces = monitors.elements[monitors.focused].workspaces;
let current_workspace = workspaces.elements[workspaces.focused];
console.log(
workspaces.elements
.map((workspace) => workspace.name)
.filter((name) => name !== null)
);
console.log(current_workspace.name);
}
});
stream.on("end", () => {
console.log("Client disconnected");
});
});
server.listen(pipeName, () => {
console.log("Named pipe server listening");
});
const command = "komorebic subscribe-pipe komorebi-js";
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Error executing command: ${error}`);
return;
}
});
```
## Unix Domain Sockets
It is possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
by `komorebi` using [Unix Domain Sockets](https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/).
UDS are also the only mode of communication between `komorebi` and `komorebic`.
First, your application must create a socket in `$ENV:LocalAppData\komorebi`. Once the socket has been created, run the
following command:
```powershell
komorebic.exe subscribe-socket <your socket name>
```
If the socket exists, komorebi will start pushing JSON data of successfully handled events and messages as in the
example above in the Named Pipes section.
## Rust Client
As of `v0.1.22` it is possible to use the `komorebi-client` crate to subscribe to notifications of
every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust codebase.
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.22"}
use anyhow::Result;
use komorebi_client::Notification;
use komorebi_client::NotificationEvent;
use komorebi_client::UnixListener;
use komorebi_client::WindowManagerEvent;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
pub fn main() -> anyhow::Result<()> {
let socket = komorebi_client::subscribe(NAME)?;
for incoming in socket.incoming() {
match incoming {
Ok(data) => {
let reader = BufReader::new(data.try_clone()?);
for line in reader.lines().flatten() {
let notification: Notification = match serde_json::from_str(&line) {
Ok(notification) => notification,
Err(error) => {
log::debug!("discarding malformed komorebi notification: {error}");
continue;
}
};
// match and filter on desired notifications
}
}
Err(error) => {
log::debug!("{error}");
}
}
}
}
```
A read-world example can be found
in [komokana](https://github.com/LGUG2Z/komokana/blob/feature/komorebi-uds/src/main.rs).
## Subscription Event Notification Schema

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

@@ -34,7 +34,5 @@ windows managed by `komorebi`.
This feature is not considered stable and you may encounter visual artifacts
from time to time.
<!-- TODO: Record a new video -->
[![Watch the tutorial
video](https://img.youtube.com/vi/ywiAvoMV_gE/hqdefault.jpg)](https://www.youtube.com/watch?v=ywiAvoMV_gE)
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)

View File

@@ -25,3 +25,6 @@ If you already have configuration files that you wish to keep, move them to the
The next time you run `komorebic start`, any files created by or loaded by
_komorebi_ will be placed or expected to exist in this folder.
[![Watch the tutorial
video](https://img.youtube.com/vi/C_KWUqQ6kko/hqdefault.jpg)](https://www.youtube.com/watch?v=C_KWUqQ6kko)

View File

@@ -2,16 +2,18 @@
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
}
```
<!-- TODO: Record a new video -->
A restart of `komorebi` is required after changing these settings.
[![Watch the tutorial video](https://img.youtube.com/vi/eGr07mymgWE/hqdefault.jpg)](https://www.youtube.com/watch?v=eGr07mymgWE)
[![Watch the tutorial video](https://img.youtube.com/vi/6QYLao953XE/hqdefault.jpg)](https://www.youtube.com/watch?v=6QYLao953XE)

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

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

View File

@@ -0,0 +1,79 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::container::Container;
pub use komorebi::monitor::Monitor;
pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::State;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::CustomLayout;
pub use komorebi_core::CycleDirection;
pub use komorebi_core::DefaultLayout;
pub use komorebi_core::Direction;
pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
use komorebi::DATA_DIR;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::Shutdown;
pub use uds_windows::UnixListener;
use uds_windows::UnixStream;
const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
}
}
Ok(())
}
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
let mut reader = BufReader::new(stream);
let mut response = String::new();
reader.read_to_string(&mut response)?;
Ok(response)
}
pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
let socket = DATA_DIR.join(name);
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {}
_ => {
return Err(error);
}
},
};
let listener = UnixListener::bind(&socket)?;
send_message(&SocketMessage::AddSubscriberSocket(name.to_string()))?;
Ok(listener)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.20"
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

@@ -131,11 +131,65 @@ impl Arrangement for DefaultLayout {
layouts
}
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap
)]
Self::Grid => {
// Shamelessly lifted from LeftWM
// https://github.com/leftwm/leftwm/blob/18675067b8450e520ef75db2ebbb0d973aa1199e/leftwm-core/src/layouts/grid_horizontal.rs
let mut layouts: Vec<Rect> = vec![];
layouts.resize(len, Rect::default());
let len = len as i32;
let num_cols = (len as f32).sqrt().ceil() as i32;
let mut iter = layouts.iter_mut().enumerate().peekable();
for col in 0..num_cols {
let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32;
let remaining_windows = len - iter_peek;
let remaining_columns = num_cols - col;
let num_rows_in_this_col = remaining_windows / remaining_columns;
let win_height = area.bottom / num_rows_in_this_col;
let win_width = area.right / num_cols;
for row in 0..num_rows_in_this_col {
if let Some((_idx, win)) = iter.next() {
let mut left = area.left + win_width * col;
let mut top = area.top + win_height * row;
match layout_flip {
Some(Axis::Horizontal) => {
left = area.right - win_width * (col + 1) + area.left;
}
Some(Axis::Vertical) => {
top = area.bottom - win_height * (row + 1) + area.top;
}
Some(Axis::HorizontalAndVertical) => {
left = area.right - win_width * (col + 1) + area.left;
top = area.bottom - win_height * (row + 1) + area.top;
}
None => {} // No flip
}
win.bottom = win_height;
win.right = win_width;
win.left = left;
win.top = top;
}
}
}
layouts
}
};
dimensions
.iter_mut()
.for_each(|l| l.add_padding(container_padding));
.for_each(|l| l.add_padding(container_padding.unwrap_or_default()));
dimensions
}
@@ -258,7 +312,7 @@ impl Arrangement for CustomLayout {
dimensions
.iter_mut()
.for_each(|l| l.add_padding(container_padding));
.for_each(|l| l.add_padding(container_padding.unwrap_or_default()));
dimensions
}

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

@@ -20,6 +20,7 @@ pub enum DefaultLayout {
VerticalStack,
HorizontalStack,
UltrawideVerticalStack,
Grid,
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
}
@@ -135,7 +136,8 @@ impl DefaultLayout {
Self::Rows => Self::VerticalStack,
Self::VerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::BSP,
Self::UltrawideVerticalStack => Self::Grid,
Self::Grid => Self::BSP,
}
}
@@ -147,7 +149,8 @@ impl DefaultLayout {
Self::HorizontalStack => Self::VerticalStack,
Self::VerticalStack => Self::Rows,
Self::Rows => Self::Columns,
Self::Columns => Self::BSP,
Self::Columns => Self::Grid,
Self::Grid => Self::BSP,
}
}
}

View File

@@ -19,10 +19,30 @@ pub trait Direction {
idx: usize,
count: usize,
) -> bool;
fn up_index(&self, idx: usize) -> usize;
fn down_index(&self, idx: usize) -> usize;
fn left_index(&self, idx: usize) -> usize;
fn right_index(&self, idx: usize) -> usize;
fn up_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize;
fn down_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize;
fn left_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize;
fn right_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize;
}
impl Direction for DefaultLayout {
@@ -35,28 +55,28 @@ impl Direction for DefaultLayout {
match op_direction {
OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.left_index(idx))
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
} else {
None
}
}
OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.right_index(idx))
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
} else {
None
}
}
OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.up_index(idx))
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
} else {
None
}
}
OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.down_index(idx))
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
} else {
None
}
@@ -77,6 +97,7 @@ impl Direction for DefaultLayout {
Self::Rows | Self::HorizontalStack => idx != 0,
Self::VerticalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx > 2,
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Down => match self {
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
@@ -85,6 +106,7 @@ impl Direction for DefaultLayout {
Self::VerticalStack => idx != 0 && idx != count - 1,
Self::HorizontalStack => idx == 0,
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Left => match self {
Self::BSP => count > 1 && idx != 0,
@@ -92,6 +114,7 @@ impl Direction for DefaultLayout {
Self::Rows => false,
Self::HorizontalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => count > 1 && idx != 1,
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Right => match self {
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
@@ -104,11 +127,17 @@ impl Direction for DefaultLayout {
2 => idx != 0,
_ => idx < 2,
},
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
}
}
fn up_index(&self, idx: usize) -> usize {
fn up_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize {
match self {
Self::BSP => {
if idx % 2 == 0 {
@@ -120,18 +149,30 @@ impl Direction for DefaultLayout {
Self::Columns => unreachable!(),
Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx - 1,
Self::HorizontalStack => 0,
Self::Grid => grid_neighbor(op_direction, idx, count),
}
}
fn down_index(&self, idx: usize) -> usize {
fn down_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize {
match self {
Self::BSP | Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx + 1,
Self::Columns => unreachable!(),
Self::HorizontalStack => 1,
Self::Grid => grid_neighbor(op_direction, idx, count),
}
}
fn left_index(&self, idx: usize) -> usize {
fn left_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize {
match self {
Self::BSP => {
if idx % 2 == 0 {
@@ -148,10 +189,16 @@ impl Direction for DefaultLayout {
1 => unreachable!(),
_ => 0,
},
Self::Grid => grid_neighbor(op_direction, idx, count),
}
}
fn right_index(&self, idx: usize) -> usize {
fn right_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize {
match self {
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
Self::Rows => unreachable!(),
@@ -161,10 +208,126 @@ impl Direction for DefaultLayout {
0 => 2,
_ => unreachable!(),
},
Self::Grid => grid_neighbor(op_direction, idx, count),
}
}
}
struct GridItem {
state: GridItemState,
row: usize,
num_rows: usize,
touching_edges: GridTouchingEdges,
}
enum GridItemState {
Valid,
Invalid,
}
#[allow(clippy::struct_excessive_bools)]
struct GridTouchingEdges {
left: bool,
right: bool,
up: bool,
down: bool,
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
fn get_grid_item(idx: usize, count: usize) -> GridItem {
let num_cols = (count as f32).sqrt().ceil() as usize;
let mut iter = 0;
for col in 0..num_cols {
let remaining_windows = count - iter;
let remaining_columns = num_cols - col;
let num_rows_in_this_col = remaining_windows / remaining_columns;
for row in 0..num_rows_in_this_col {
if iter == idx {
return GridItem {
state: GridItemState::Valid,
row: row + 1,
num_rows: num_rows_in_this_col,
touching_edges: GridTouchingEdges {
left: col == 0,
right: col == num_cols - 1,
up: row == 0,
down: row == num_rows_in_this_col - 1,
},
};
}
iter += 1;
}
}
GridItem {
state: GridItemState::Invalid,
row: 0,
num_rows: 0,
touching_edges: GridTouchingEdges {
left: true,
right: true,
up: true,
down: true,
},
}
}
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
let item = get_grid_item(idx, count);
match item.state {
GridItemState::Invalid => false,
GridItemState::Valid => match op_direction {
OperationDirection::Left => item.touching_edges.left,
OperationDirection::Right => item.touching_edges.right,
OperationDirection::Up => item.touching_edges.up,
OperationDirection::Down => item.touching_edges.down,
},
}
}
fn grid_neighbor(
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
) -> usize {
let Some(op_direction) = op_direction else {
return 0;
};
let Some(count) = count else {
return 0;
};
let item = get_grid_item(idx, count);
match op_direction {
OperationDirection::Left => {
let item_from_prev_col = get_grid_item(idx - item.row, count);
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
return idx - (item.num_rows - 1);
}
if item.num_rows != item_from_prev_col.num_rows && !item.touching_edges.down {
return idx - (item.num_rows - 1);
}
idx - item.num_rows
}
OperationDirection::Right => idx + item.num_rows,
OperationDirection::Up => idx - 1,
OperationDirection::Down => idx + 1,
}
}
impl Direction for CustomLayout {
fn index_in_direction(
&self,
@@ -179,28 +342,28 @@ impl Direction for CustomLayout {
match op_direction {
OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.left_index(idx))
Option::from(self.left_index(None, idx, None))
} else {
None
}
}
OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.right_index(idx))
Option::from(self.right_index(None, idx, None))
} else {
None
}
}
OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.up_index(idx))
Option::from(self.up_index(None, idx, None))
} else {
None
}
}
OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.down_index(idx))
Option::from(self.down_index(None, idx, None))
} else {
None
}
@@ -254,15 +417,30 @@ impl Direction for CustomLayout {
}
}
fn up_index(&self, idx: usize) -> usize {
fn up_index(
&self,
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
) -> usize {
idx - 1
}
fn down_index(&self, idx: usize) -> usize {
fn down_index(
&self,
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
) -> usize {
idx + 1
}
fn left_index(&self, idx: usize) -> usize {
fn left_index(
&self,
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
) -> usize {
let column_idx = self.column_for_container_idx(idx);
if column_idx - 1 == 0 {
0
@@ -271,7 +449,12 @@ impl Direction for CustomLayout {
}
}
fn right_index(&self, idx: usize) -> usize {
fn right_index(
&self,
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
) -> usize {
let column_idx = self.column_for_container_idx(idx);
self.first_container_idx(column_idx + 1)
}

View File

@@ -156,8 +156,10 @@ pub enum SocketMessage {
ToggleMouseFollowsFocus,
RemoveTitleBar(ApplicationIdentifier, String),
ToggleTitleBars,
AddSubscriber(String),
RemoveSubscriber(String),
AddSubscriberSocket(String),
RemoveSubscriberSocket(String),
AddSubscriberPipe(String),
RemoveSubscriberPipe(String),
ApplicationSpecificConfigurationSchema,
NotificationSchema,
SocketSchema,
@@ -221,6 +223,8 @@ pub enum ApplicationIdentifier {
Class,
#[serde(alias = "title")]
Title,
#[serde(alias = "path")]
Path,
}
#[derive(

View File

@@ -27,13 +27,20 @@ impl From<RECT> for Rect {
}
impl Rect {
pub fn add_padding(&mut self, padding: Option<i32>) {
if let Some(padding) = padding {
self.left += padding;
self.top += padding;
self.right -= padding * 2;
self.bottom -= padding * 2;
}
/// decrease the size of self by the padding amount.
pub fn add_padding(&mut self, padding: i32) {
self.left += padding;
self.top += padding;
self.right -= padding * 2;
self.bottom -= padding * 2;
}
/// increase the size of self by the margin amount.
pub fn add_margin(&mut self, margin: i32) {
self.left -= margin;
self.top -= margin;
self.right += margin * 2;
self.bottom += margin * 2;
}
#[must_use]
@@ -43,4 +50,14 @@ impl Rect {
&& point.1 >= self.top
&& point.1 <= self.top + self.bottom
}
#[must_use]
pub const fn scale(&self, system_dpi: i32, rect_dpi: i32) -> Rect {
Rect {
left: (self.left * rect_dpi) / system_dpi,
top: (self.top * rect_dpi) / system_dpi,
right: (self.right * rect_dpi) / system_dpi,
bottom: (self.bottom * rect_dpi) / system_dpi,
}
}
}

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.20"
version = "0.1.23-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -15,37 +15,38 @@ komorebi-core = { path = "../komorebi-core" }
bitflags = "2"
clap = { version = "4", features = ["derive"] }
color-eyre = { workspace = true }
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
ctrlc = "3"
dirs = { workspace = true }
getset = "0.1"
hotwatch = "0.4"
hex_color = { version = "3", features = ["serde"] }
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"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uds_windows = "1"
which = "5"
which = "6"
widestring = "1"
windows = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.52"
windows-interface = { workspace = true }
windows-implement = { workspace = true }
windows = { workspace = true }
color-eyre = { workspace = true }
dirs = { workspace = true }
widestring = "1"
[features]
deadlock_detection = []

View File

@@ -1,5 +1,4 @@
use std::sync::atomic::Ordering;
use std::time::Duration;
use color_eyre::Result;
use windows::core::PCWSTR;
@@ -12,19 +11,14 @@ use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use komorebi_core::Rect;
use crate::window::should_act;
use crate::window::Window;
use crate::windows_callbacks;
use crate::WindowsApi;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::BORDER_RECT;
use crate::REGEX_IDENTIFIERS;
use crate::BORDER_WIDTH;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_11;
#[derive(Debug, Clone, Copy)]
pub struct Border {
@@ -68,7 +62,6 @@ impl Border {
unsafe {
while GetMessageW(&mut message, border.hwnd(), 0, 0).into() {
DispatchMessageW(&message);
std::thread::sleep(Duration::from_millis(10));
}
}
@@ -82,10 +75,6 @@ impl Border {
BORDER_HWND.store(hwnd.0, Ordering::SeqCst);
if *WINDOWS_11 {
WindowsApi::round_corners(hwnd.0)?;
}
Ok(())
}
@@ -97,12 +86,7 @@ impl Border {
}
}
pub fn set_position(
self,
window: Window,
invisible_borders: &Rect,
activate: bool,
) -> Result<()> {
pub fn set_position(self, window: Window, activate: bool) -> Result<()> {
if self.hwnd == 0 {
Ok(())
} else {
@@ -111,38 +95,10 @@ impl Border {
}
let mut rect = WindowsApi::window_rect(window.hwnd())?;
rect.top -= invisible_borders.bottom;
rect.bottom += invisible_borders.bottom;
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
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 should_expand_border {
rect.left -= invisible_borders.left;
rect.top -= invisible_borders.top;
rect.right += invisible_borders.right;
rect.bottom += invisible_borders.bottom;
}
let border_offset = BORDER_OFFSET.lock();
if let Some(border_offset) = *border_offset {
rect.left -= border_offset.left;
rect.top -= border_offset.top;
rect.right += border_offset.right;
rect.bottom += border_offset.bottom;
}
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
rect.add_margin(border_width);
*BORDER_RECT.lock() = rect;

103
komorebi/src/colour.rs Normal file
View File

@@ -0,0 +1,103 @@
use hex_color::HexColor;
use schemars::gen::SchemaGenerator;
use schemars::schema::InstanceType;
use schemars::schema::Schema;
use schemars::schema::SchemaObject;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Colour {
/// Colour represented as RGB
Rgb(Rgb),
/// Colour represented as Hex
Hex(Hex),
}
impl From<Rgb> for Colour {
fn from(value: Rgb) -> Self {
Self::Rgb(value)
}
}
impl From<u32> for Colour {
fn from(value: u32) -> Self {
Self::Rgb(Rgb::from(value))
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Hex(HexColor);
impl JsonSchema for Hex {
fn schema_name() -> String {
String::from("Hex")
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
..Default::default()
}
.into()
}
}
impl From<Colour> for u32 {
fn from(value: Colour) -> Self {
match value {
Colour::Rgb(val) => val.into(),
Colour::Hex(val) => (Rgb::from(val)).into(),
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Rgb {
/// Red
pub r: u32,
/// Green
pub g: u32,
/// Blue
pub b: u32,
}
impl Rgb {
pub fn new(r: u32, g: u32, b: u32) -> Self {
Self { r, g, b }
}
}
impl From<Hex> for Rgb {
fn from(value: Hex) -> Self {
value.0.into()
}
}
impl From<HexColor> for Rgb {
fn from(value: HexColor) -> Self {
Self {
r: value.r as u32,
g: value.g as u32,
b: value.b as u32,
}
}
}
impl From<Rgb> for u32 {
fn from(value: Rgb) -> Self {
value.r | (value.g << 8) | (value.b << 16)
}
}
impl From<u32> for Rgb {
fn from(value: u32) -> Self {
Self {
r: value & 0xff,
g: value >> 8 & 0xff,
b: value >> 16 & 0xff,
}
}
}

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

@@ -3,14 +3,14 @@ use std::collections::VecDeque;
use getset::Getters;
use nanoid::nanoid;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone, Serialize, Getters, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
#[serde(skip_serializing)]
#[getset(get = "pub")]
id: String,
windows: Ring<Window>,

View File

@@ -1,5 +1,4 @@
use std::sync::atomic::Ordering;
use std::time::Duration;
use color_eyre::Result;
use windows::core::PCWSTR;
@@ -59,7 +58,6 @@ impl Hidden {
unsafe {
while GetMessageW(&mut message, hidden.hwnd(), 0, 0).into() {
DispatchMessageW(&message);
std::thread::sleep(Duration::from_millis(10));
}
}

360
komorebi/src/lib.rs Normal file
View File

@@ -0,0 +1,360 @@
pub mod border;
pub mod com;
#[macro_use]
pub mod ring;
pub mod colour;
pub mod container;
pub mod hidden;
pub mod monitor;
pub mod process_command;
pub mod process_event;
pub mod process_movement;
pub mod set_window_position;
pub mod static_config;
pub mod styles;
pub mod window;
pub mod window_manager;
pub mod window_manager_event;
pub mod windows_api;
pub mod windows_callbacks;
pub mod winevent;
pub mod winevent_listener;
pub mod workspace;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::net::TcpStream;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use hidden::*;
pub use process_command::*;
pub use process_event::*;
pub use static_config::*;
pub use window_manager::*;
pub use window_manager_event::*;
pub use windows_api::WindowsApi;
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;
use komorebi_core::Rect;
use komorebi_core::SocketMessage;
use os_info::Version;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixStream;
use which::which;
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
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<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<MatchingRule>>> =
Arc::new(Mutex::new(vec![
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("explorer.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("chrome.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("idea64.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("ApplicationFrameHost.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
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<MatchingRule>>> = Arc::new(Mutex::new(vec![
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
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()));
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
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<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
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: String::from("OPContainerClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
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<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(),
]));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
Arc::new(Mutex::new(HidingBehaviour::Minimize));
pub static ref HOME_DIR: PathBuf = {
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
);
}
})
};
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
pub static ref AHK_EXE: String = {
let mut ahk: String = String::from("autohotkey.exe");
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
if which(&komorebi_ahk_exe).is_ok() {
ahk = komorebi_ahk_exe;
}
}
ahk
};
static ref WINDOWS_11: bool = {
matches!(
os_info::get().version(),
Version::Semantic(_, _, x) if x >= &22000
)
};
static ref BORDER_RECT: Arc<Mutex<Rect>> =
Arc::new(Mutex::new(Rect::default()));
// 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![]));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false);
pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0);
pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false);
pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
pub const TRANSPARENCY_COLOUR: u32 = 0;
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// This is the path on Windows 10
let mut current = hkcu
.open_subkey(format!(
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
SESSION_ID.load(Ordering::SeqCst)
))
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
// This is the path on Windows 11
if current.is_none() {
current = hkcu
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
}
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
// exist until one has been created in the task view
// The registry value will also not exist on user login if virtual desktops have been created
// but the task view has not been initiated
// In both of these cases, we return None, and the virtual desktop validation will never run. In
// the latter case, if the user desires this validation after initiating the task view, komorebi
// should be restarted, and then when this // fn runs again for the first time, it will pick up
// the value of CurrentVirtualDesktop and validate against it accordingly
current
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Notification {
pub event: NotificationEvent,
pub state: State,
}
pub fn notify_subscribers(notification: &str) -> Result<()> {
let mut stale_sockets = vec![];
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
for (socket, path) in &mut *sockets {
match UnixStream::connect(path) {
Ok(mut stream) => {
tracing::debug!("pushed notification to subscriber: {socket}");
stream.write_all(notification.as_bytes())?;
}
Err(_) => {
stale_sockets.push(socket.clone());
}
}
}
for socket in stale_sockets {
tracing::warn!("removing stale subscription: {socket}");
sockets.remove(&socket);
}
let mut stale_pipes = vec![];
let mut pipes = SUBSCRIPTION_PIPES.lock();
for (subscriber, pipe) in &mut *pipes {
match writeln!(pipe, "{notification}") {
Ok(()) => {
tracing::debug!("pushed notification to subscriber: {subscriber}");
}
Err(error) => {
// ERROR_FILE_NOT_FOUND
// 2 (0x2)
// The system cannot find the file specified.
// ERROR_NO_DATA
// 232 (0xE8)
// The pipe is being closed.
// Remove the subscription; the process will have to subscribe again
if let Some(2 | 232) = error.raw_os_error() {
stale_pipes.push(subscriber.clone());
}
}
}
}
for subscriber in stale_pipes {
tracing::warn!("removing stale subscription: {}", subscriber);
pipes.remove(&subscriber);
}
Ok(())
}
pub fn load_configuration() -> Result<()> {
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");
if config_pwsh.exists() {
let powershell_exe = if which("pwsh.exe").is_ok() {
"pwsh.exe"
} else {
"powershell.exe"
};
tracing::info!("loading configuration file: {}", config_pwsh.display());
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());
Command::new(&*AHK_EXE)
.arg(config_ahk.as_os_str())
.output()?;
}
Ok(())
}

View File

@@ -6,16 +6,7 @@
clippy::significant_drop_in_scrutinee
)]
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::net::TcpStream;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
#[cfg(feature = "deadlock_detection")]
@@ -23,222 +14,29 @@ use std::time::Duration;
use clap::Parser;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::Backoff;
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;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use which::which;
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;
use crate::process_command::listen_for_commands;
use crate::process_command::listen_for_commands_tcp;
use crate::process_event::listen_for_events;
use crate::process_movement::listen_for_movements;
use crate::static_config::StaticConfig;
use crate::window_manager::State;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
#[macro_use]
mod ring;
mod border;
mod com;
mod container;
mod hidden;
mod monitor;
mod process_command;
mod process_event;
mod process_movement;
mod set_window_position;
mod static_config;
mod styles;
mod window;
mod window_manager;
mod window_manager_event;
mod windows_api;
mod windows_callbacks;
mod winevent;
mod winevent_listener;
mod workspace;
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>>> =
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),
}
]));
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 MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
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![
// 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),
}
]));
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 WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"X410.exe".to_string(),
"vcxsrv.exe".to_string(),
]));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
Arc::new(Mutex::new(HidingBehaviour::Minimize));
static ref HOME_DIR: PathBuf = {
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
);
}
})
};
static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
static ref AHK_EXE: String = {
let mut ahk: String = String::from("autohotkey.exe");
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
if which(&komorebi_ahk_exe).is_ok() {
ahk = komorebi_ahk_exe;
}
}
ahk
};
static ref WINDOWS_11: bool = {
matches!(
os_info::get().version(),
Version::Semantic(_, _, x) if x >= &22000
)
};
static ref BORDER_RECT: Arc<Mutex<Rect>> =
Arc::new(Mutex::new(Rect::default()));
static ref BORDER_OFFSET: Arc<Mutex<Option<Rect>>> =
Arc::new(Mutex::new(None));
// 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![]));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static ALT_FOCUS_HACK: AtomicBool = AtomicBool::new(false);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false);
pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0);
pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false);
pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(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 HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);
use komorebi::hidden::Hidden;
use komorebi::load_configuration;
use komorebi::process_command::listen_for_commands;
use komorebi::process_command::listen_for_commands_tcp;
use komorebi::process_event::listen_for_events;
use komorebi::process_movement::listen_for_movements;
use komorebi::static_config::StaticConfig;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
use komorebi::winevent_listener;
use komorebi::CUSTOM_FFM;
use komorebi::HOME_DIR;
use komorebi::INITIAL_CONFIGURATION_LOADED;
use komorebi::SESSION_ID;
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
@@ -303,124 +101,6 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
Ok((guard, color_guard))
}
pub fn load_configuration() -> Result<()> {
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");
if config_pwsh.exists() {
let powershell_exe = if which("pwsh.exe").is_ok() {
"pwsh.exe"
} else {
"powershell.exe"
};
tracing::info!("loading configuration file: {}", config_pwsh.display());
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());
Command::new(&*AHK_EXE)
.arg(config_ahk.as_os_str())
.output()?;
}
Ok(())
}
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// This is the path on Windows 10
let mut current = hkcu
.open_subkey(format!(
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
SESSION_ID.load(Ordering::SeqCst)
))
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
// This is the path on Windows 11
if current.is_none() {
current = hkcu
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
.ok()
.and_then(
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
Ok(current) => Option::from(current.bytes),
Err(_) => None,
},
);
}
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
// exist until one has been created in the task view
// The registry value will also not exist on user login if virtual desktops have been created
// but the task view has not been initiated
// In both of these cases, we return None, and the virtual desktop validation will never run. In
// the latter case, if the user desires this validation after initiating the task view, komorebi
// should be restarted, and then when this // fn runs again for the first time, it will pick up
// the value of CurrentVirtualDesktop and validate against it accordingly
current
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct Notification {
pub event: NotificationEvent,
pub state: State,
}
pub fn notify_subscribers(notification: &str) -> Result<()> {
let mut stale_subscriptions = vec![];
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
for (subscriber, pipe) in &mut *subscriptions {
match writeln!(pipe, "{notification}") {
Ok(()) => {
tracing::debug!("pushed notification to subscriber: {}", subscriber);
}
Err(error) => {
// ERROR_FILE_NOT_FOUND
// 2 (0x2)
// The system cannot find the file specified.
// ERROR_NO_DATA
// 232 (0xE8)
// The pipe is being closed.
// Remove the subscription; the process will have to subscribe again
if let Some(2 | 232) = error.raw_os_error() {
let subscriber_cl = subscriber.clone();
stale_subscriptions.push(subscriber_cl);
}
}
}
}
for subscriber in stale_subscriptions {
tracing::warn!("removing stale subscription: {}", subscriber);
subscriptions.remove(&subscriber);
}
Ok(())
}
#[cfg(feature = "deadlock_detection")]
#[tracing::instrument]
fn detect_deadlocks() {
@@ -482,8 +162,8 @@ fn main() -> Result<()> {
if matched_procs.len() > 1 {
let mut len = matched_procs.len();
for proc in matched_procs {
if let Some(root) = proc.root() {
if root.ends_with("shims") {
if let Some(executable_path) = proc.exe() {
if executable_path.to_string_lossy().contains("shims") {
len -= 1;
}
}
@@ -500,15 +180,11 @@ fn main() -> Result<()> {
WindowsApi::foreground_lock_timeout()?;
winevent_listener::start();
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
Hidden::create("komorebi-hidden")?;
let static_config = opts.config.map_or_else(
@@ -531,12 +207,12 @@ fn main() -> Result<()> {
Arc::new(Mutex::new(StaticConfig::preload(
config,
Arc::new(Mutex::new(incoming)),
winevent_listener::event_rx(),
)?))
} else {
Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
incoming,
)))?))
Arc::new(Mutex::new(WindowManager::new(
winevent_listener::event_rx(),
)?))
};
wm.lock().init()?;

View File

@@ -9,6 +9,7 @@ use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Rect;
@@ -17,7 +18,9 @@ use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
#[derive(
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
id: isize,
@@ -34,10 +37,9 @@ pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
work_area_offset: Option<Rect>,
workspaces: Ring<Workspace>,
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
last_focused_workspace: Option<usize>,
#[serde(skip_serializing)]
#[getset(get_mut = "pub")]
workspace_names: HashMap<usize, String>,
}
@@ -190,11 +192,7 @@ impl Monitor {
self.workspaces().len()
}
pub fn update_focused_workspace(
&mut self,
offset: Option<Rect>,
invisible_borders: &Rect,
) -> Result<()> {
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
let work_area = *self.work_area_size();
let offset = if self.work_area_offset().is_some() {
self.work_area_offset()
@@ -204,7 +202,7 @@ impl Monitor {
self.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?
.update(&work_area, offset, invisible_borders)?;
.update(&work_area, offset)?;
Ok(())
}

View File

@@ -4,7 +4,6 @@ use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::TcpListener;
use std::net::TcpStream;
use std::num::NonZeroUsize;
@@ -24,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;
@@ -39,6 +39,7 @@ use komorebi_core::WindowContainerBehaviour;
use komorebi_core::WindowKind;
use crate::border::Border;
use crate::colour::Rgb;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::static_config::StaticConfig;
@@ -48,7 +49,6 @@ use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::Notification;
use crate::NotificationEvent;
use crate::ALT_FOCUS_HACK;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
@@ -72,6 +72,7 @@ use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::TCP_CONNECTIONS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
@@ -144,8 +145,15 @@ pub fn listen_for_commands_tcp(wm: Arc<Mutex<WindowManager>>, port: usize) {
}
impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn process_command(&mut self, message: SocketMessage) -> Result<()> {
// TODO(raggi): wrap reply in a newtype that can decorate a human friendly
// name for the peer, such as getting the pid of the komorebic process for
// the UDS or the IP:port for TCP.
#[tracing::instrument(skip(self, reply))]
pub fn process_command(
&mut self,
message: SocketMessage,
mut reply: impl std::io::Write,
) -> Result<()> {
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
if let Some(id) = current_virtual_desktop() {
if id != *virtual_desktop_id {
@@ -266,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) => {
@@ -284,20 +294,21 @@ 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 invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let mut hwnds_to_purge = vec![];
@@ -309,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));
@@ -340,7 +356,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no focused workspace"))?
.remove_window(hwnd)?;
monitor.update_focused_workspace(offset, &invisible_borders)?;
monitor.update_focused_workspace(offset)?;
}
}
SocketMessage::FocusedWorkspaceContainerPadding(adjustment) => {
@@ -743,15 +759,11 @@ impl WindowManager {
Err(error) => error.to_string(),
};
let socket = DATA_DIR.join("komorebic.sock");
tracing::info!("replying to state");
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(state.as_bytes())?;
}
}
reply.write_all(state.as_bytes())?;
tracing::info!("replying to state done");
}
SocketMessage::VisibleWindows => {
let mut monitor_visible_windows = HashMap::new();
@@ -774,15 +786,7 @@ impl WindowManager {
Err(error) => error.to_string(),
};
let socket = DATA_DIR.join("komorebic.sock");
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(visible_windows_state.as_bytes())?;
}
}
reply.write_all(visible_windows_state.as_bytes())?;
}
SocketMessage::Query(query) => {
@@ -801,15 +805,7 @@ impl WindowManager {
}
.to_string();
let socket = DATA_DIR.join("komorebic.sock");
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(response.as_bytes())?;
}
}
reply.write_all(response.as_bytes())?;
}
SocketMessage::ResizeWindowEdge(direction, sizing) => {
self.resize_window(direction, sizing, self.resize_delta, true)?;
@@ -1034,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) => {
@@ -1052,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) => {
@@ -1087,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 => {
@@ -1106,10 +1110,7 @@ impl WindowManager {
SocketMessage::UnmanageFocusedWindow => {
self.unmanage_focused_window()?;
}
SocketMessage::InvisibleBorders(rect) => {
self.invisible_borders = rect;
self.retile_all(false)?;
}
SocketMessage::InvisibleBorders(_rect) => {}
SocketMessage::WorkAreaOffset(rect) => {
self.work_area_offset = Option::from(rect);
self.retile_all(false)?;
@@ -1170,7 +1171,16 @@ impl WindowManager {
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
}
SocketMessage::AddSubscriber(ref subscriber) => {
SocketMessage::AddSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
let socket_path = DATA_DIR.join(socket);
sockets.insert(socket.clone(), socket_path);
}
SocketMessage::RemoveSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
sockets.remove(socket);
}
SocketMessage::AddSubscriberPipe(ref subscriber) => {
let mut pipes = SUBSCRIPTION_PIPES.lock();
let pipe_path = format!(r"\\.\pipe\{subscriber}");
let pipe = connect(&pipe_path).map_err(|_| {
@@ -1179,7 +1189,7 @@ impl WindowManager {
pipes.insert(subscriber.clone(), pipe);
}
SocketMessage::RemoveSubscriber(ref subscriber) => {
SocketMessage::RemoveSubscriberPipe(ref subscriber) => {
let mut pipes = SUBSCRIPTION_PIPES.lock();
pipes.remove(subscriber);
}
@@ -1238,14 +1248,14 @@ impl WindowManager {
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => {
match kind {
WindowKind::Single => {
BORDER_COLOUR_SINGLE.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
BORDER_COLOUR_SINGLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Stack => {
BORDER_COLOUR_STACK.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
BORDER_COLOUR_STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Monocle => {
BORDER_COLOUR_MONOCLE.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
BORDER_COLOUR_MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
}
@@ -1256,60 +1266,29 @@ impl WindowManager {
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::ActiveWindowBorderOffset(offset) => {
let mut current_border_offset = BORDER_OFFSET.lock();
let new_border_offset = Rect {
left: offset,
top: offset,
right: offset * 2,
bottom: offset * 2,
};
*current_border_offset = Option::from(new_border_offset);
BORDER_OFFSET.store(offset, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::AltFocusHack(enable) => {
ALT_FOCUS_HACK.store(enable, Ordering::SeqCst);
SocketMessage::AltFocusHack(_) => {
tracing::info!("this action is deprecated");
}
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 connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(schema.as_bytes())?;
}
}
reply.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 connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(schema.as_bytes())?;
}
}
reply.write_all(schema.as_bytes())?;
}
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 connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(schema.as_bytes())?;
}
}
reply.write_all(schema.as_bytes())?;
}
SocketMessage::StaticConfigSchema => {
let settings = SchemaSettings::default().with(|s| {
@@ -1321,27 +1300,13 @@ impl WindowManager {
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<StaticConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
let socket = DATA_DIR.join("komorebic.sock");
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(schema.as_bytes())?;
}
}
reply.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 connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(config.as_bytes())?;
}
}
reply.write_all(config.as_bytes())?;
}
SocketMessage::RemoveTitleBar(_, ref id) => {
let mut identifiers = NO_TITLEBAR.lock();
@@ -1428,9 +1393,6 @@ impl WindowManager {
| SocketMessage::FocusWorkspaceNumber(_) => {
let foreground = WindowsApi::foreground_window()?;
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;
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
if monocle != 0 && self.focused_workspace()?.monocle_container().is_some() {
@@ -1440,14 +1402,18 @@ 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));
border.set_position(foreground_window, &self.invisible_borders, false)?;
border.set_position(foreground_window, false)?;
}
SocketMessage::TogglePause => {
let is_paused = self.is_paused;
@@ -1457,7 +1423,7 @@ impl WindowManager {
border.hide()?;
} else {
let focused = self.focused_window()?;
border.set_position(*focused, &self.invisible_borders, true)?;
border.set_position(*focused, true)?;
focused.focus(false)?;
}
}
@@ -1467,7 +1433,7 @@ impl WindowManager {
if tiling_enabled {
let focused = self.focused_window()?;
border.set_position(*focused, &self.invisible_borders, true)?;
border.set_position(*focused, true)?;
focused.focus(false)?;
} else {
border.hide()?;
@@ -1526,9 +1492,13 @@ impl WindowManager {
}
}
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) -> Result<()> {
let stream = BufReader::new(stream);
for line in stream.lines() {
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream) -> Result<()> {
let reader = BufReader::new(stream.try_clone()?);
// TODO(raggi): while this processes more than one command, if there are
// replies there is no clearly defined protocol for framing yet - it's
// perhaps whole-json objects for now, but termination is signalled by
// socket shutdown.
for line in reader.lines() {
let message = SocketMessage::from_str(&line?)?;
let mut wm = wm.lock();
@@ -1536,7 +1506,7 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) ->
if wm.is_paused {
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message)?)
Ok(wm.process_command(message, &mut stream)?)
}
_ => {
tracing::trace!("ignoring while paused");
@@ -1545,7 +1515,7 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) ->
};
}
wm.process_command(message.clone())?;
wm.process_command(message.clone(), &mut stream)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: wm.as_ref().into(),
@@ -1560,11 +1530,11 @@ pub fn read_commands_tcp(
stream: &mut TcpStream,
addr: &str,
) -> Result<()> {
let mut stream = BufReader::new(stream);
let mut reader = BufReader::new(stream.try_clone()?);
loop {
let mut buf = vec![0; 1024];
match stream.read(&mut buf) {
match reader.read(&mut buf) {
Err(..) => {
tracing::warn!("removing disconnected tcp client: {addr}");
let mut connections = TCP_CONNECTIONS.lock();
@@ -1585,7 +1555,7 @@ pub fn read_commands_tcp(
if wm.is_paused {
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message)?)
Ok(wm.process_command(message, stream)?)
}
_ => {
tracing::trace!("ignoring while paused");
@@ -1594,7 +1564,7 @@ pub fn read_commands_tcp(
};
}
wm.process_command(message.clone())?;
wm.process_command(message.clone(), &mut *stream)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: wm.as_ref().into(),

View File

@@ -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;
@@ -35,7 +37,7 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
let receiver = wm.lock().incoming_events.lock().clone();
let receiver = wm.lock().incoming_events.clone();
std::thread::spawn(move || {
tracing::info!("listening");
@@ -109,7 +111,6 @@ impl WindowManager {
_ => {}
}
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
@@ -123,7 +124,7 @@ impl WindowManager {
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, &invisible_borders)?;
workspace.update(&work_area, offset)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
@@ -146,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) => {
@@ -186,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.
@@ -194,6 +196,7 @@ impl WindowManager {
title,
exe_name,
class,
path,
&tray_and_multi_window_identifiers,
&regex_identifiers,
);
@@ -303,21 +306,24 @@ impl WindowManager {
}
}
WindowManagerEvent::MoveResizeStart(_, window) => {
let monitor_idx = self.focused_monitor_idx();
let workspace_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace_idx();
let container_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
.focused_container_idx();
if *self.focused_workspace()?.tile() {
let monitor_idx = self.focused_monitor_idx();
let workspace_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace_idx();
let container_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
.focused_container_idx();
WindowsApi::bring_window_to_top(window.hwnd())?;
WindowsApi::bring_window_to_top(window.hwnd())?;
self.pending_move_op = Option::from((monitor_idx, workspace_idx, container_idx));
self.pending_move_op =
Option::from((monitor_idx, workspace_idx, container_idx));
}
}
WindowManagerEvent::MoveResizeEnd(_, window) => {
// We need this because if the event ends on a different monitor,
@@ -331,7 +337,6 @@ impl WindowManager {
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
let new_window_behaviour = self.window_container_behaviour;
let invisible_borders = self.invisible_borders;
let workspace = self.focused_workspace_mut()?;
if !workspace
@@ -341,7 +346,7 @@ impl WindowManager {
{
let focused_container_idx = workspace.focused_container_idx();
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
let new_position = WindowsApi::window_rect(window.hwnd())?;
let old_position = *workspace
.latest_layout()
@@ -366,12 +371,6 @@ impl WindowManager {
}
}
// Adjust for the invisible borders
new_position.left += invisible_borders.left;
new_position.top += invisible_borders.top;
new_position.right -= invisible_borders.right;
new_position.bottom -= invisible_borders.bottom;
let resize = Rect {
left: new_position.left - old_position.left,
top: new_position.top - old_position.top,
@@ -381,10 +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 right_bottom_constant = ((BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst))
* 2)
.abs();
let is_move = moved_across_monitors
|| resize.right == 0 && resize.bottom == 0
|| resize.right.abs() == invisible_borders.right
&& resize.bottom.abs() == invisible_borders.bottom;
|| resize.right.abs() == right_bottom_constant
&& resize.bottom.abs() == right_bottom_constant;
if is_move {
tracing::info!("moving with mouse");
@@ -476,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));
@@ -496,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));
}
@@ -518,6 +524,12 @@ impl WindowManager {
| WindowManagerEvent::Uncloak(..) => {}
};
if !self.focused_workspace()?.tile() {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
if *self.focused_workspace()?.tile() && BORDER_ENABLED.load(Ordering::SeqCst) {
match event {
WindowManagerEvent::MoveResizeStart(_, _) => {
@@ -529,6 +541,7 @@ impl WindowManager {
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Hide(_, window)
| WindowManagerEvent::Uncloak(_, window)
| WindowManagerEvent::Minimize(_, window) => {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
let mut target_window = None;
@@ -585,15 +598,10 @@ impl WindowManager {
}
if let Some(target_window) = target_window {
let window = target_window;
let mut rect = WindowsApi::window_rect(window.hwnd())?;
rect.top -= self.invisible_borders.bottom;
rect.bottom += self.invisible_borders.bottom;
let activate = BORDER_HIDDEN.load(Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
border.set_position(target_window, &self.invisible_borders, activate)?;
border.set_position(target_window, activate)?;
if activate {
BORDER_HIDDEN.store(false, Ordering::SeqCst);
@@ -606,7 +614,7 @@ impl WindowManager {
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
if let WindowManagerEvent::Unmanage(window) = event {
window.center(&self.focused_monitor_work_area()?, &invisible_borders)?;
window.center(&self.focused_monitor_work_area()?)?;
}
// If there are no more windows on the workspace, we shouldn't show the border window

View File

@@ -1,9 +1,10 @@
use std::collections::VecDeque;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Ring<T> {
elements: VecDeque<T>,
focused: usize,

View File

@@ -1,4 +1,5 @@
use crate::border::Border;
use crate::colour::Colour;
use crate::current_virtual_desktop;
use crate::monitor::Monitor;
use crate::ring::Ring;
@@ -28,13 +29,16 @@ 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 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;
@@ -62,34 +66,14 @@ use std::sync::Arc;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Rgb {
/// Red
pub r: u32,
/// Green
pub g: u32,
/// Blue
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
pub single: Rgb,
pub single: Colour,
/// Border colour when the container contains multiple windows
pub stack: Rgb,
pub stack: Colour,
/// Border colour when the container is in monocle mode
pub monocle: Rgb,
pub monocle: Colour,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -232,7 +216,7 @@ impl From<&Monitor> for MonitorConfig {
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.20`
pub struct StaticConfig {
/// Dimensions of Windows' own invisible borders; don't set these yourself unless you are told to
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
pub invisible_borders: Option<Rect>,
/// Delta to resize windows by (default 50)
@@ -256,12 +240,14 @@ 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>,
/// Width of the active window border (default: 20)
/// Width of the window border (default: 8)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_width: Option<i32>,
/// Offset of the active window border (default: None)
#[serde(alias = "active_window_border_width")]
pub border_width: Option<i32>,
/// Offset of the window border (default: -1)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_offset: Option<i32>,
#[serde(alias = "active_window_border_offset")]
pub border_offset: Option<i32>,
/// Display an active window border (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border: Option<bool>,
@@ -277,10 +263,6 @@ pub struct StaticConfig {
/// Monitor and workspace configurations
#[serde(skip_serializing_if = "Option::is_none")]
pub monitors: Option<Vec<MonitorConfig>>,
/// DEPRECATED from v0.1.20: no longer required
#[schemars(skip)]
#[serde(skip_serializing)]
pub alt_focus_hack: Option<bool>,
/// Which Windows signal to use when hiding windows (default: minimize)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_hiding_behaviour: Option<HidingBehaviour>,
@@ -289,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>>,
@@ -316,13 +298,6 @@ pub struct StaticConfig {
impl From<&WindowManager> for StaticConfig {
#[allow(clippy::too_many_lines)]
fn from(value: &WindowManager) -> Self {
let default_invisible_borders = Rect {
left: 7,
top: 0,
right: 14,
bottom: 7,
};
let mut monitors = vec![];
for m in value.monitors() {
monitors.push(MonitorConfig::from(m));
@@ -377,13 +352,13 @@ impl From<&WindowManager> for StaticConfig {
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 {
single: Colour::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
stack: Colour::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
}),
monocle: Rgb::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
monocle: Colour::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
@@ -392,11 +367,7 @@ impl From<&WindowManager> for StaticConfig {
};
Self {
invisible_borders: if value.invisible_borders == default_invisible_borders {
None
} else {
Option::from(value.invisible_borders)
},
invisible_borders: None,
resize_delta: Option::from(value.resize_delta),
window_container_behaviour: Option::from(value.window_container_behaviour),
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
@@ -406,10 +377,8 @@ 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: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: Option::from(BORDER_OFFSET.load(Ordering::SeqCst)),
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
active_window_border_colours: border_colours,
default_workspace_padding: Option::from(
@@ -419,7 +388,6 @@ impl From<&WindowManager> for StaticConfig {
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
),
monitors: Option::from(monitors),
alt_focus_hack: None,
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
global_work_area_offset: value.work_area_offset,
float_rules: None,
@@ -460,49 +428,22 @@ impl StaticConfig {
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
}
self.active_window_border_width.map_or_else(
self.border_width.map_or_else(
|| {
BORDER_WIDTH.store(20, Ordering::SeqCst);
BORDER_WIDTH.store(8, 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,
};
let mut border_offset = BORDER_OFFSET.lock();
*border_offset = Some(new_border_offset);
},
);
BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
if let Some(colours) = &self.active_window_border_colours {
BORDER_COLOUR_SINGLE.store(
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
Ordering::SeqCst,
);
BORDER_COLOUR_CURRENT.store(
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
Ordering::SeqCst,
);
BORDER_COLOUR_STACK.store(
colours.stack.r | (colours.stack.g << 8) | (colours.stack.b << 16),
Ordering::SeqCst,
);
BORDER_COLOUR_MONOCLE.store(
colours.monocle.r | (colours.monocle.g << 8) | (colours.monocle.b << 16),
Ordering::SeqCst,
);
BORDER_COLOUR_SINGLE.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_STACK.store(u32::from(colours.stack), Ordering::SeqCst);
BORDER_COLOUR_MONOCLE.store(u32::from(colours.monocle), Ordering::SeqCst);
}
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
@@ -513,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 {
@@ -621,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,
)?;
}
}
}
@@ -748,7 +551,7 @@ impl StaticConfig {
#[allow(clippy::too_many_lines)]
pub fn preload(
path: &PathBuf,
incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>,
incoming: Receiver<WindowManagerEvent>,
) -> Result<WindowManager> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
@@ -775,12 +578,6 @@ impl StaticConfig {
incoming_events: incoming,
command_listener: listener,
is_paused: false,
invisible_borders: value.invisible_borders.unwrap_or(Rect {
left: 7,
top: 0,
right: 14,
bottom: 7,
}),
virtual_desktop_id: current_virtual_desktop(),
work_area_offset: value.global_work_area_offset,
window_container_behaviour: value
@@ -811,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");
@@ -927,10 +724,6 @@ impl StaticConfig {
wm.hide_border()?;
}
if let Some(val) = value.invisible_borders {
wm.invisible_borders = val;
}
if let Some(val) = value.window_container_behaviour {
wm.window_container_behaviour = val;
}
@@ -963,6 +756,76 @@ impl StaticConfig {
wm.focus_follows_mouse = value.focus_follows_mouse;
let monitor_count = wm.monitors().len();
for i in 0..monitor_count {
wm.update_focused_workspace_by_monitor_idx(i)?;
}
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,23 +4,22 @@ 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;
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;
use serde::ser::Error;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use windows::Win32::Foundation::HWND;
use winput::press;
use winput::release;
use winput::Vk;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
@@ -30,8 +29,6 @@ use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::ALT_FOCUS_HACK;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
@@ -42,7 +39,7 @@ use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy, JsonSchema)]
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
pub struct Window {
pub(crate) hwnd: isize,
}
@@ -128,7 +125,7 @@ impl Window {
HWND(self.hwnd)
}
pub fn center(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
@@ -139,45 +136,23 @@ impl Window {
right: half_width,
bottom: half_weight,
},
invisible_borders,
true,
)
}
pub fn set_position(
&mut self,
layout: &Rect,
invisible_borders: &Rect,
top: bool,
) -> Result<()> {
let mut rect = *layout;
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 should_remove_border {
// Remove the invisible borders
rect.left -= invisible_borders.left;
rect.top -= invisible_borders.top;
rect.right += invisible_borders.right;
rect.bottom += invisible_borders.bottom;
}
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
let rect = *layout;
WindowsApi::position_window(self.hwnd(), &rect, top)
}
pub fn is_maximized(self) -> bool {
WindowsApi::is_zoomed(self.hwnd())
}
pub fn is_miminized(self) -> bool {
WindowsApi::is_iconic(self.hwnd())
}
pub fn hide(self) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
@@ -242,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());
@@ -316,12 +240,7 @@ impl Window {
let mut tried_resetting_foreground_access = false;
let mut max_attempts = 10;
let hotkey_uses_alt = WindowsApi::alt_is_pressed();
while !foregrounded && max_attempts > 0 {
if ALT_FOCUS_HACK.load(Ordering::SeqCst) {
press(Vk::Alt);
}
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {
foregrounded = true;
@@ -342,10 +261,6 @@ impl Window {
}
}
};
if ALT_FOCUS_HACK.load(Ordering::SeqCst) && !hotkey_uses_alt {
release(Vk::Alt);
}
}
// Center cursor in Window
@@ -413,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)?;
@@ -476,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));
}
}
_ => {}
@@ -491,6 +414,7 @@ fn window_is_eligible(
title: &String,
exe_name: &String,
class: &String,
path: &str,
style: &WindowStyle,
ex_style: &ExtendedWindowStyle,
event: Option<WindowManagerEvent>,
@@ -509,6 +433,7 @@ fn window_is_eligible(
title,
exe_name,
class,
path,
&float_identifiers,
&regex_identifiers,
);
@@ -518,6 +443,7 @@ fn window_is_eligible(
title,
exe_name,
class,
path,
&manage_identifiers,
&regex_identifiers,
);
@@ -531,6 +457,7 @@ fn window_is_eligible(
title,
exe_name,
class,
path,
&layered_whitelist,
&regex_identifiers,
);
@@ -548,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
@@ -558,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
@@ -570,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,14 +12,16 @@ 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;
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;
@@ -44,7 +46,7 @@ use crate::static_config::StaticConfig;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
use crate::winevent_listener;
use crate::workspace::Workspace;
use crate::BORDER_HWND;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
@@ -65,10 +67,9 @@ use crate::WORKSPACE_RULES;
pub struct WindowManager {
pub monitors: Ring<Monitor>,
pub monitor_cache: HashMap<usize, Monitor>,
pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>,
pub incoming_events: Receiver<WindowManagerEvent>,
pub command_listener: UnixListener,
pub is_paused: bool,
pub invisible_borders: Rect,
pub work_area_offset: Option<Rect>,
pub resize_delta: i32,
pub window_container_behaviour: WindowContainerBehaviour,
@@ -84,11 +85,10 @@ pub struct WindowManager {
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct State {
pub monitors: Ring<Monitor>,
pub is_paused: bool,
pub invisible_borders: Rect,
pub resize_delta: i32,
pub new_window_behaviour: WindowContainerBehaviour,
pub cross_monitor_move_behaviour: MoveBehaviour,
@@ -97,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>,
}
@@ -118,7 +118,6 @@ impl From<&WindowManager> for State {
Self {
monitors: wm.monitors.clone(),
is_paused: wm.is_paused,
invisible_borders: wm.invisible_borders,
work_area_offset: wm.work_area_offset,
resize_delta: wm.resize_delta,
new_window_behaviour: wm.window_container_behaviour,
@@ -167,7 +166,7 @@ impl EnforceWorkspaceRuleOp {
impl WindowManager {
#[tracing::instrument]
pub fn new(incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>) -> Result<Self> {
pub fn new(incoming: Receiver<WindowManagerEvent>) -> Result<Self> {
let socket = DATA_DIR.join("komorebi.sock");
match std::fs::remove_file(&socket) {
@@ -189,12 +188,6 @@ impl WindowManager {
incoming_events: incoming,
command_listener: listener,
is_paused: false,
invisible_borders: Rect {
left: 7,
top: 0,
right: 14,
bottom: 7,
},
virtual_desktop_id: current_virtual_desktop(),
work_area_offset: None,
window_container_behaviour: WindowContainerBehaviour::Create,
@@ -219,15 +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 };
let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?;
rect.top -= self.invisible_borders.bottom;
rect.bottom += self.invisible_borders.bottom;
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, &self.invisible_borders, 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))]
@@ -272,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");
});
@@ -402,7 +396,6 @@ impl WindowManager {
}
}
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
for monitor in self.monitors_mut() {
@@ -433,7 +426,7 @@ impl WindowManager {
}
if should_update {
monitor.update_focused_workspace(offset, &invisible_borders)?;
monitor.update_focused_workspace(offset)?;
}
}
@@ -635,7 +628,6 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn retile_all(&mut self, preserve_resize_dimensions: bool) -> Result<()> {
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
for monitor in self.monitors_mut() {
@@ -657,7 +649,7 @@ impl WindowManager {
}
}
workspace.update(&work_area, offset, &invisible_borders)?;
workspace.update(&work_area, offset)?;
}
Ok(())
@@ -667,75 +659,56 @@ impl WindowManager {
pub fn manage_focused_window(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let event = WindowManagerEvent::Manage(Window { hwnd });
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
Ok(winevent_listener::event_tx().send(event)?)
}
#[tracing::instrument(skip(self))]
pub fn unmanage_focused_window(&mut self) -> Result<()> {
let hwnd = WindowsApi::foreground_window()?;
let event = WindowManagerEvent::Unmanage(Window { hwnd });
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
Ok(winevent_listener::event_tx().send(event)?)
}
#[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_CALLBACK_CHANNEL.lock().0.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))]
@@ -829,12 +802,11 @@ impl WindowManager {
pub fn update_focused_workspace(&mut self, follow_focus: bool) -> Result<()> {
tracing::info!("updating");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
self.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?
.update_focused_workspace(offset, &invisible_borders)?;
.update_focused_workspace(offset)?;
if follow_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
@@ -865,6 +837,30 @@ impl WindowManager {
}
}
// 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
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)?;
}
}
};
// This is to correctly restore and focus when switching to a workspace which
// contains a managed maximized window
if !follow_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
window.restore();
window.focus(self.mouse_follows_focus)?;
}
}
Ok(())
}
@@ -999,13 +995,12 @@ impl WindowManager {
}
pub fn update_focused_workspace_by_monitor_idx(&mut self, idx: usize) -> Result<()> {
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
self.monitors_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no monitor"))?
.update_focused_workspace(offset, &invisible_borders)
.update_focused_workspace(offset)
}
#[tracing::instrument(skip(self))]
@@ -1095,7 +1090,6 @@ impl WindowManager {
tracing::info!("moving container");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let mouse_follows_focus = self.mouse_follows_focus;
@@ -1114,7 +1108,7 @@ impl WindowManager {
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
monitor.update_focused_workspace(offset, &invisible_borders)?;
monitor.update_focused_workspace(offset)?;
let target_monitor = self
.monitors_mut()
@@ -1123,7 +1117,7 @@ impl WindowManager {
target_monitor.add_container(container, workspace_idx)?;
target_monitor.load_focused_workspace(mouse_follows_focus)?;
target_monitor.update_focused_workspace(offset, &invisible_borders)?;
target_monitor.update_focused_workspace(offset)?;
if follow {
self.focus_monitor(monitor_idx)?;
@@ -1305,12 +1299,11 @@ impl WindowManager {
// make sure to update the origin monitor workspace layout because it is no
// longer focused so it won't get updated at the end of this fn
let offset = self.work_area_offset;
let invisible_borders = self.invisible_borders;
self.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
.update_focused_workspace(offset, &invisible_borders)?;
.update_focused_workspace(offset)?;
let a = self
.focused_monitor()
@@ -1443,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
@@ -1460,9 +1455,15 @@ impl WindowManager {
pub fn promote_container_to_front(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
if matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid)) {
tracing::debug!("ignoring promote command for grid layout");
return Ok(());
}
tracing::info!("promoting container");
let workspace = self.focused_workspace_mut()?;
workspace.promote_container()?;
self.update_focused_workspace(self.mouse_follows_focus)
}
@@ -1471,9 +1472,15 @@ impl WindowManager {
pub fn promote_focus_to_front(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
if matches!(workspace.layout(), Layout::Default(DefaultLayout::Grid)) {
tracing::info!("ignoring promote focus command for grid layout");
return Ok(());
}
tracing::info!("promoting focus");
let workspace = self.focused_workspace_mut()?;
let target_idx = match workspace.layout() {
Layout::Default(_) => 0,
Layout::Custom(custom) => custom
@@ -1534,7 +1541,6 @@ impl WindowManager {
tracing::info!("floating window");
let work_area = self.focused_monitor_work_area()?;
let invisible_borders = self.invisible_borders;
let workspace = self.focused_workspace_mut()?;
workspace.new_floating_window()?;
@@ -1544,7 +1550,7 @@ impl WindowManager {
.last_mut()
.ok_or_else(|| anyhow!("there is no floating window"))?;
window.center(&work_area, &invisible_borders)?;
window.center(&work_area)?;
window.focus(self.mouse_follows_focus)?;
Ok(())
@@ -1620,10 +1626,10 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn flip_layout(&mut self, layout_flip: Axis) -> Result<()> {
tracing::info!("flipping layout");
let workspace = self.focused_workspace_mut()?;
tracing::info!("flipping layout");
#[allow(clippy::match_same_arms)]
match workspace.layout_flip() {
None => {
@@ -1801,7 +1807,6 @@ impl WindowManager {
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
@@ -1830,7 +1835,7 @@ impl WindowManager {
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
@@ -1850,7 +1855,6 @@ impl WindowManager {
{
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
@@ -1881,7 +1885,7 @@ impl WindowManager {
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
@@ -1896,7 +1900,6 @@ impl WindowManager {
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
@@ -1923,7 +1926,7 @@ impl WindowManager {
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
@@ -1939,7 +1942,6 @@ impl WindowManager {
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
@@ -1965,7 +1967,7 @@ impl WindowManager {
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
@@ -1984,7 +1986,6 @@ impl WindowManager {
{
tracing::info!("setting workspace layout");
let layout = CustomLayout::from_path(path)?;
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
@@ -2011,7 +2012,7 @@ impl WindowManager {
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
workspace.update(&work_area, offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)

View File

@@ -2,6 +2,7 @@ use std::fmt::Display;
use std::fmt::Formatter;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::window::should_act;
@@ -10,7 +11,7 @@ use crate::winevent::WinEvent;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
#[derive(Debug, Copy, Clone, Serialize, JsonSchema)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "content")]
pub enum WindowManagerEvent {
Destroy(WinEvent, Window),
@@ -138,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

@@ -22,6 +22,7 @@ use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
use windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
@@ -82,6 +83,7 @@ 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::IsZoomed;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
@@ -100,7 +102,7 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HWND_BOTTOM;
use windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
@@ -125,8 +127,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
@@ -264,7 +265,7 @@ impl WindowsApi {
}
.ok()
{
Ok(_) => {}
Ok(()) => {}
Err(error) => {
tracing::error!("enum_display_devices: {}", error);
return Err(error.into());
@@ -339,14 +340,25 @@ impl WindowsApi {
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
}
/// position window resizes the target window to the given layout, adjusting
/// the layout to account for any window shadow borders (the window painted
/// region will match layout on completion).
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
let flags = SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_COPY_BITS
| SetWindowPosition::FRAME_CHANGED;
let position = if top { HWND_TOPMOST } else { HWND_BOTTOM };
Self::set_window_pos(hwnd, layout, position, flags.bits())
let shadow_rect = Self::shadow_rect(hwnd)?;
let rect = Rect {
left: layout.left + shadow_rect.left,
top: layout.top + shadow_rect.top,
right: layout.right + shadow_rect.right,
bottom: layout.bottom + shadow_rect.bottom,
};
let position = if top { HWND_TOP } else { HWND_NOTOPMOST };
Self::set_window_pos(hwnd, &rect, position, flags.bits())
}
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
@@ -356,7 +368,7 @@ impl WindowsApi {
pub fn raise_window(hwnd: HWND) -> Result<()> {
let flags = SetWindowPosition::NO_MOVE;
let position = HWND_TOPMOST;
let position = HWND_TOP;
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
}
@@ -367,6 +379,18 @@ impl WindowsApi {
SetWindowPosition::NO_ACTIVATE
};
// TODO(raggi): This leaves the window behind the active window, which
// can result e.g. single pixel window borders being invisible in the
// case of opaque window borders (e.g. EPIC Games Launcher). Ideally
// we'd be able to pass a parent window to place ourselves just in front
// of, however the SetWindowPos API explicitly ignores that parameter
// unless the window being positioned is being activated - and we don't
// want to activate the border window here. We can hopefully find a
// better workaround in the future.
// The trade-off chosen prevents the border window from sitting over the
// top of other pop-up dialogs such as a file picker dialog from
// Firefox. When adjusting this in the future, it's important to check
// those dialog cases.
let position = HWND_NOTOPMOST;
Self::set_window_pos(hwnd, layout, position, flags.bits())
}
@@ -378,7 +402,8 @@ impl WindowsApi {
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
}
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
unsafe {
SetWindowPos(
hwnd,
@@ -470,9 +495,36 @@ impl WindowsApi {
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
let mut rect = unsafe { std::mem::zeroed() };
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
Ok(Rect::from(rect))
if Self::dwm_get_window_attribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut rect).is_ok() {
// TODO(raggi): once we declare DPI awareness, we will need to scale the rect.
// let window_scale = unsafe { GetDpiForWindow(hwnd) };
// let system_scale = unsafe { GetDpiForSystem() };
// Ok(Rect::from(rect).scale(system_scale.try_into()?, window_scale.try_into()?))
Ok(Rect::from(rect))
} else {
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
Ok(Rect::from(rect))
}
}
/// shadow_rect computes the offset of the shadow position of the window to
/// the window painted region. The four values in the returned Rect can be
/// added to a position rect to compute a size for set_window_pos that will
/// fill the target area, ignoring shadows.
fn shadow_rect(hwnd: HWND) -> Result<Rect> {
let window_rect = Self::window_rect(hwnd)?;
let mut srect = Default::default();
unsafe { GetWindowRect(hwnd, &mut srect) }.process()?;
let shadow_rect = Rect::from(srect);
Ok(Rect {
left: shadow_rect.left - window_rect.left,
top: shadow_rect.top - window_rect.top,
right: shadow_rect.right - window_rect.right,
bottom: shadow_rect.bottom - window_rect.bottom,
})
}
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
@@ -677,6 +729,10 @@ impl WindowsApi {
unsafe { IsIconic(hwnd) }.into()
}
pub fn is_zoomed(hwnd: HWND) -> bool {
unsafe { IsZoomed(hwnd) }.into()
}
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFOEXW> {
let mut ex_info = MONITORINFOEXW::default();
ex_info.monitorInfo.cbSize = u32::try_from(std::mem::size_of::<MONITORINFOEXW>())?;
@@ -844,10 +900,10 @@ impl WindowsApi {
pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
name,
name,
WS_POPUP | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
WS_POPUP | WS_SYSMENU,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,

View File

@@ -13,11 +13,13 @@ use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
@@ -37,13 +39,15 @@ use crate::ring::Ring;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_RECT;
use crate::BORDER_WIDTH;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_11;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
@@ -146,12 +150,17 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let is_visible = WindowsApi::is_window_visible(hwnd);
let is_window = WindowsApi::is_window(hwnd);
let is_minimized = WindowsApi::is_iconic(hwnd);
let is_maximized = WindowsApi::is_zoomed(hwnd);
if is_visible && is_window && !is_minimized {
let window = Window { hwnd: hwnd.0 };
if let Ok(should_manage) = window.should_manage(None) {
if should_manage {
if is_maximized {
WindowsApi::restore_window(hwnd);
}
let mut container = Container::default();
container.windows_mut().push_back(window);
containers.push_back(container);
@@ -178,7 +187,10 @@ pub extern "system" fn win_event_hook(
let window = Window { hwnd: hwnd.0 };
let winevent = unsafe { ::std::mem::transmute(event) };
let winevent = match WinEvent::try_from(event) {
Ok(event) => event,
Err(_) => return,
};
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => return,
Some(event) => event,
@@ -186,11 +198,9 @@ pub extern "system" fn win_event_hook(
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
if should_manage {
WINEVENT_CALLBACK_CHANNEL
.lock()
.0
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
.expect("could not send message on winevent_listener::event_tx");
}
}
}
@@ -208,7 +218,7 @@ pub extern "system" fn border_window(
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
let hpen = CreatePen(
PS_SOLID,
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(BORDER_COLOUR_CURRENT.load(Ordering::SeqCst)),
);
@@ -216,7 +226,17 @@ pub extern "system" fn border_window(
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
// TODO(raggi): this is approximately the correct curvature for
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
// often the bottom right has a different shape. Furthermore if
// the window was made with DWMWCP_ROUNDSMALL then this is the
// wrong size. In the future we should read the DWM properties
// of windows and attempt to match appropriately.
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, border_rect.right, border_rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
}
EndPaint(window, &ps);
ValidateRect(window, None);
@@ -241,11 +261,9 @@ pub extern "system" fn hidden_window(
match message {
WM_DISPLAYCHANGE => {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
WINEVENT_CALLBACK_CHANNEL
.lock()
.0
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
.expect("could not send message on winevent_listener::event_tx");
LRESULT(0)
}
@@ -256,11 +274,9 @@ pub extern "system" fn hidden_window(
|| wparam.0 as u32 == SPI_ICONVERTICALSPACING.0
{
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
WINEVENT_CALLBACK_CHANNEL
.lock()
.0
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}
@@ -269,11 +285,9 @@ pub extern "system" fn hidden_window(
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
WINEVENT_CALLBACK_CHANNEL
.lock()
.0
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}

View File

@@ -1,6 +1,7 @@
#![allow(clippy::use_self)]
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
@@ -88,7 +89,7 @@ use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Display, JsonSchema)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Display, JsonSchema)]
#[repr(u32)]
#[allow(dead_code)]
pub enum WinEvent {
@@ -177,3 +178,100 @@ pub enum WinEvent {
UiaPropIdSEnd = EVENT_UIA_PROPID_END,
UiaPropIdStart = EVENT_UIA_PROPID_START,
}
impl TryFrom<u32> for WinEvent {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
EVENT_AIA_END => Ok(Self::AiaEnd),
EVENT_AIA_START => Ok(Self::AiaStart),
EVENT_CONSOLE_CARET => Ok(Self::ConsoleCaret),
EVENT_CONSOLE_END => Ok(Self::ConsoleEnd),
EVENT_CONSOLE_END_APPLICATION => Ok(Self::ConsoleEndApplication),
EVENT_CONSOLE_LAYOUT => Ok(Self::ConsoleLayout),
EVENT_CONSOLE_START_APPLICATION => Ok(Self::ConsoleStartApplication),
EVENT_CONSOLE_UPDATE_REGION => Ok(Self::ConsoleUpdateRegion),
EVENT_CONSOLE_UPDATE_SCROLL => Ok(Self::ConsoleUpdateScroll),
EVENT_CONSOLE_UPDATE_SIMPLE => Ok(Self::ConsoleUpdateSimple),
EVENT_OBJECT_ACCELERATORCHANGE => Ok(Self::ObjectAcceleratorChange),
EVENT_OBJECT_CLOAKED => Ok(Self::ObjectCloaked),
EVENT_OBJECT_CONTENTSCROLLED => Ok(Self::ObjectContentScrolled),
EVENT_OBJECT_CREATE => Ok(Self::ObjectCreate),
EVENT_OBJECT_DEFACTIONCHANGE => Ok(Self::ObjectDefActionChange),
EVENT_OBJECT_DESCRIPTIONCHANGE => Ok(Self::ObjectDescriptionChange),
EVENT_OBJECT_DESTROY => Ok(Self::ObjectDestroy),
EVENT_OBJECT_DRAGCANCEL => Ok(Self::ObjectDragCancel),
EVENT_OBJECT_DRAGCOMPLETE => Ok(Self::ObjectDragComplete),
EVENT_OBJECT_DRAGDROPPED => Ok(Self::ObjectDragDropped),
EVENT_OBJECT_DRAGENTER => Ok(Self::ObjectDragEnter),
EVENT_OBJECT_DRAGLEAVE => Ok(Self::ObjectDragLeave),
EVENT_OBJECT_DRAGSTART => Ok(Self::ObjectDragStart),
EVENT_OBJECT_END => Ok(Self::ObjectEnd),
EVENT_OBJECT_FOCUS => Ok(Self::ObjectFocus),
EVENT_OBJECT_HELPCHANGE => Ok(Self::ObjectHelpChange),
EVENT_OBJECT_HIDE => Ok(Self::ObjectHide),
EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED => Ok(Self::ObjectHostedObjectsInvalidated),
EVENT_OBJECT_IME_CHANGE => Ok(Self::ObjectImeChange),
EVENT_OBJECT_IME_HIDE => Ok(Self::ObjectImeHide),
EVENT_OBJECT_IME_SHOW => Ok(Self::ObjectImeShow),
EVENT_OBJECT_INVOKED => Ok(Self::ObjectInvoked),
EVENT_OBJECT_LIVEREGIONCHANGED => Ok(Self::ObjectLiveRegionChanged),
EVENT_OBJECT_LOCATIONCHANGE => Ok(Self::ObjectLocationChange),
EVENT_OBJECT_NAMECHANGE => Ok(Self::ObjectNameChange),
EVENT_OBJECT_PARENTCHANGE => Ok(Self::ObjectParentChange),
EVENT_OBJECT_REORDER => Ok(Self::ObjectReorder),
EVENT_OBJECT_SELECTION => Ok(Self::ObjectSelection),
EVENT_OBJECT_SELECTIONADD => Ok(Self::ObjectSelectionAdd),
EVENT_OBJECT_SELECTIONREMOVE => Ok(Self::ObjectSelectionRemove),
EVENT_OBJECT_SELECTIONWITHIN => Ok(Self::ObjectSelectionWithin),
EVENT_OBJECT_SHOW => Ok(Self::ObjectShow),
EVENT_OBJECT_STATECHANGE => Ok(Self::ObjectStateChange),
EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED => {
Ok(Self::ObjectTextEditConversionTargetChanged)
}
EVENT_OBJECT_TEXTSELECTIONCHANGED => Ok(Self::ObjectTextSelectionChanged),
EVENT_OBJECT_UNCLOAKED => Ok(Self::ObjectUncloaked),
EVENT_OBJECT_VALUECHANGE => Ok(Self::ObjectValueChange),
EVENT_OEM_DEFINED_END => Ok(Self::OemDefinedEnd),
EVENT_OEM_DEFINED_START => Ok(Self::OemDefinedStart),
EVENT_SYSTEM_ALERT => Ok(Self::SystemAlert),
EVENT_SYSTEM_ARRANGMENTPREVIEW => Ok(Self::SystemArrangementPreview),
EVENT_SYSTEM_CAPTUREEND => Ok(Self::SystemCaptureEnd),
EVENT_SYSTEM_CAPTURESTART => Ok(Self::SystemCaptureStart),
EVENT_SYSTEM_CONTEXTHELPEND => Ok(Self::SystemContextHelpEnd),
EVENT_SYSTEM_CONTEXTHELPSTART => Ok(Self::SystemContextHelpStart),
EVENT_SYSTEM_DESKTOPSWITCH => Ok(Self::SystemDesktopSwitch),
EVENT_SYSTEM_DIALOGEND => Ok(Self::SystemDialogEnd),
EVENT_SYSTEM_DIALOGSTART => Ok(Self::SystemDialogStart),
EVENT_SYSTEM_DRAGDROPEND => Ok(Self::SystemDragDropEnd),
EVENT_SYSTEM_DRAGDROPSTART => Ok(Self::SystemDragDropStart),
EVENT_SYSTEM_END => Ok(Self::SystemEnd),
EVENT_SYSTEM_FOREGROUND => Ok(Self::SystemForeground),
EVENT_SYSTEM_IME_KEY_NOTIFICATION => Ok(Self::SystemImeKeyNotification),
EVENT_SYSTEM_MENUEND => Ok(Self::SystemMenuEnd),
EVENT_SYSTEM_MENUPOPUPEND => Ok(Self::SystemMenuPopupEnd),
EVENT_SYSTEM_MENUPOPUPSTART => Ok(Self::SystemMenuPopupStart),
EVENT_SYSTEM_MENUSTART => Ok(Self::SystemMenuStart),
EVENT_SYSTEM_MINIMIZEEND => Ok(Self::SystemMinimizeEnd),
EVENT_SYSTEM_MINIMIZESTART => Ok(Self::SystemMinimizeStart),
EVENT_SYSTEM_MOVESIZEEND => Ok(Self::SystemMoveSizeEnd),
EVENT_SYSTEM_MOVESIZESTART => Ok(Self::SystemMoveSizeStart),
EVENT_SYSTEM_SCROLLINGEND => Ok(Self::SystemScrollingEnd),
EVENT_SYSTEM_SCROLLINGSTART => Ok(Self::SystemScrollingStart),
EVENT_SYSTEM_SOUND => Ok(Self::SystemSound),
EVENT_SYSTEM_SWITCHEND => Ok(Self::SystemSwitchEnd),
EVENT_SYSTEM_SWITCHER_APPDROPPED => Ok(Self::SystemSwitcherAppDropped),
EVENT_SYSTEM_SWITCHER_APPGRABBED => Ok(Self::SystemSwitcherAppGrabbed),
EVENT_SYSTEM_SWITCHER_APPOVERTARGET => Ok(Self::SystemSwitcherAppOverTarget),
EVENT_SYSTEM_SWITCHER_CANCELLED => Ok(Self::SystemSwitcherCancelled),
EVENT_SYSTEM_SWITCHSTART => Ok(Self::SystemSwitchStart),
EVENT_UIA_EVENTID_END => Ok(Self::UiaEventIdSEnd),
EVENT_UIA_EVENTID_START => Ok(Self::UiaEventIdStart),
EVENT_UIA_PROPID_END => Ok(Self::UiaPropIdSEnd),
EVENT_UIA_PROPID_START => Ok(Self::UiaPropIdStart),
_ => Err(()),
}
}
}

View File

@@ -1,105 +1,62 @@
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use std::sync::OnceLock;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Accessibility::SetWinEventHook;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_callbacks;
lazy_static! {
pub static ref WINEVENT_CALLBACK_CHANNEL: Arc<Mutex<(Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>)>> =
Arc::new(Mutex::new(crossbeam_channel::unbounded()));
}
static CHANNEL: OnceLock<(Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>)> =
OnceLock::new();
#[derive(Debug, Clone)]
pub struct WinEventListener {
hook: Arc<AtomicIsize>,
outgoing_events: Arc<Mutex<Sender<WindowManagerEvent>>>,
}
static EVENT_PUMP: OnceLock<std::thread::JoinHandle<()>> = OnceLock::new();
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> WinEventListener {
WinEventListener {
hook: Arc::new(AtomicIsize::new(0)),
outgoing_events: outgoing,
}
}
impl WinEventListener {
pub fn start(self) {
let hook = self.hook.clone();
let outgoing = self.outgoing_events.lock().clone();
std::thread::spawn(move || unsafe {
let hook_ref = SetWinEventHook(
EVENT_MIN,
EVENT_MAX,
None,
Some(windows_callbacks::win_event_hook),
0,
0,
0,
);
hook.store(hook_ref.0, Ordering::SeqCst);
// The code in the callback doesn't work in its own loop, needs to be within
// the MessageLoop callback for the winevent callback to even fire
MessageLoop::start(10, |_msg| {
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().1.try_recv() {
match outgoing.send(event) {
Ok(()) => {}
Err(error) => {
tracing::error!("{}", error);
}
}
}
true
});
});
}
}
#[derive(Debug, Copy, Clone)]
pub struct MessageLoop;
impl MessageLoop {
pub fn start(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
Self::start_with_sleep(sleep, cb);
}
fn start_with_sleep(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
let mut msg: MSG = MSG::default();
loop {
let mut value: Option<MSG> = None;
pub fn start() {
EVENT_PUMP.get_or_init(|| {
std::thread::spawn(move || {
unsafe {
if !bool::from(!PeekMessageW(&mut msg, HWND(0), 0, 0, PM_REMOVE)) {
SetWinEventHook(
EVENT_MIN,
EVENT_MAX,
None,
Some(windows_callbacks::win_event_hook),
0,
0,
0,
)
};
loop {
let mut msg: MSG = MSG::default();
unsafe {
if !GetMessageW(&mut msg, HWND(0), 0, 0).as_bool() {
tracing::info!("windows event processing shutdown");
break;
};
TranslateMessage(&msg);
DispatchMessageW(&msg);
value = Some(msg);
}
}
std::thread::sleep(Duration::from_millis(sleep));
if !cb(value) {
break;
}
}
}
})
});
}
fn channel() -> &'static (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) {
CHANNEL.get_or_init(crossbeam_channel::unbounded)
}
pub fn event_tx() -> Sender<WindowManagerEvent> {
channel().0.clone()
}
pub fn event_rx() -> Receiver<WindowManagerEvent> {
channel().1.clone()
}

View File

@@ -9,6 +9,7 @@ use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Axis;
@@ -25,25 +26,30 @@ use crate::static_config::WorkspaceConfig;
use crate::window::Window;
use crate::window::WindowDetails;
use crate::windows_api::WindowsApi;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::NO_TITLEBAR;
use crate::REMOVE_TITLEBARS;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
#[allow(clippy::struct_field_names)]
#[derive(
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Workspace {
#[getset(get = "pub", set = "pub")]
name: Option<String>,
containers: Ring<Container>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
monocle_container: Option<Container>,
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
monocle_container_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
maximized_window: Option<Window>,
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
maximized_window_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub")]
@@ -58,7 +64,6 @@ pub struct Workspace {
workspace_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
container_padding: Option<i32>,
#[serde(skip_serializing)]
#[getset(get = "pub", set = "pub")]
latest_layout: Vec<Rect>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
@@ -198,12 +203,7 @@ impl Workspace {
Ok(())
}
pub fn update(
&mut self,
work_area: &Rect,
offset: Option<Rect>,
invisible_borders: &Rect,
) -> Result<()> {
pub fn update(&mut self, work_area: &Rect, offset: Option<Rect>) -> Result<()> {
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
return Ok(());
}
@@ -222,7 +222,7 @@ impl Workspace {
},
);
adjusted_work_area.add_padding(self.workspace_padding());
adjusted_work_area.add_padding(self.workspace_padding().unwrap_or_default());
self.enforce_resize_constraints();
@@ -244,11 +244,19 @@ impl Workspace {
}
}
let managed_maximized_window = self.maximized_window().is_some();
if *self.tile() {
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window_mut() {
adjusted_work_area.add_padding(container_padding);
window.set_position(&adjusted_work_area, invisible_borders, true)?;
adjusted_work_area.add_padding(container_padding.unwrap_or_default());
{
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
adjusted_work_area.add_padding(border_offset);
let width = BORDER_WIDTH.load(Ordering::SeqCst);
adjusted_work_area.add_padding(width);
}
window.set_position(&adjusted_work_area, true)?;
};
} else if let Some(window) = self.maximized_window_mut() {
window.maximize();
@@ -277,7 +285,22 @@ impl Workspace {
window.add_title_bar()?;
}
window.set_position(layout, invisible_borders, false)?;
// If a window has been unmaximized via toggle-maximize, this block
// will make sure that it is unmaximized via restore_window
if window.is_maximized() && !managed_maximized_window {
WindowsApi::restore_window(window.hwnd());
}
let mut rect = *layout;
{
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
rect.add_padding(border_offset);
let width = BORDER_WIDTH.load(Ordering::SeqCst);
rect.add_padding(width);
}
window.set_position(&rect, false)?;
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.19"
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.20"
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,15 +21,15 @@ 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"
uds_windows = "1"
which = "5"
which = "6"
windows = { workspace = true }

View File

@@ -5,8 +5,9 @@ use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::ErrorKind;
use std::io::Read;
use std::io::Write;
use std::net::Shutdown;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
@@ -20,6 +21,7 @@ use clap::ValueEnum;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use dirs::data_local_dir;
use fs_tail::TailedFile;
use heck::ToKebabCase;
use komorebi_core::resolve_home_path;
@@ -29,7 +31,6 @@ use miette::Report;
use miette::SourceOffset;
use miette::SourceSpan;
use paste::paste;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
use which::which;
use windows::Win32::Foundation::HWND;
@@ -113,7 +114,7 @@ trait AhkFunction {
struct ConfigurationError {
message: String,
#[source_code]
src: NamedSource,
src: NamedSource<String>,
#[label("This bit here")]
bad_bit: SourceSpan,
}
@@ -720,13 +721,25 @@ struct LoadCustomLayout {
}
#[derive(Parser, AhkFunction)]
struct Subscribe {
struct SubscribeSocket {
/// Name of the socket to send event notifications to
socket: String,
}
#[derive(Parser, AhkFunction)]
struct UnsubscribeSocket {
/// Name of the socket to stop sending event notifications to
socket: String,
}
#[derive(Parser, AhkFunction)]
struct SubscribePipe {
/// Name of the pipe to send event notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}
#[derive(Parser, AhkFunction)]
struct Unsubscribe {
struct UnsubscribePipe {
/// Name of the pipe to stop sending event notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}
@@ -792,8 +805,14 @@ enum SubCommand {
Start(Start),
/// Stop the komorebi.exe process and restore all hidden windows
Stop(Stop),
/// Output various important komorebi-related environment values
/// Check komorebi configuration and related files for common errors
Check,
/// Show the path to komorebi.json
#[clap(alias = "config")]
Configuration,
/// Show the path to whkdrc
#[clap(alias = "whkd")]
Whkdrc,
/// Show a JSON representation of the current window manager state
State,
/// Show a JSON representation of visible windows
@@ -801,12 +820,20 @@ enum SubCommand {
/// Query the current window manager state
#[clap(arg_required_else_help = true)]
Query(Query),
/// Subscribe to komorebi events
/// Subscribe to komorebi events using a Unix Domain Socket
#[clap(arg_required_else_help = true)]
Subscribe(Subscribe),
SubscribeSocket(SubscribeSocket),
/// Unsubscribe from komorebi events
#[clap(arg_required_else_help = true)]
Unsubscribe(Unsubscribe),
UnsubscribeSocket(UnsubscribeSocket),
/// Subscribe to komorebi events using a Named Pipe
#[clap(arg_required_else_help = true)]
#[clap(alias = "subscribe")]
SubscribePipe(SubscribePipe),
/// Unsubscribe from komorebi events
#[clap(arg_required_else_help = true)]
#[clap(alias = "unsubscribe")]
UnsubscribePipe(UnsubscribePipe),
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
Log,
/// Quicksave the current resize layout dimensions
@@ -1050,8 +1077,9 @@ enum SubCommand {
WatchConfiguration(WatchConfiguration),
/// Signal that the final configuration option has been sent
CompleteConfiguration,
/// Enable or disable a hack simulating ALT key presses to ensure focus changes succeed
/// DEPRECATED since v0.1.22
#[clap(arg_required_else_help = true)]
#[clap(hide = true)]
AltFocusHack(AltFocusHack),
/// Set the window behaviour when switching workspaces / cycling stacks
#[clap(arg_required_else_help = true)]
@@ -1160,46 +1188,31 @@ enum SubCommand {
pub fn send_message(bytes: &[u8]) -> Result<()> {
let socket = DATA_DIR.join("komorebi.sock");
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(bytes)?;
}
}
Ok(())
let mut stream = UnixStream::connect(socket)?;
stream.write_all(bytes)?;
Ok(stream.shutdown(Shutdown::Write)?)
}
fn with_komorebic_socket<F: Fn() -> Result<()>>(f: F) -> Result<()> {
let socket = DATA_DIR.join("komorebic.sock");
pub fn send_query(bytes: &[u8]) -> Result<String> {
let socket = DATA_DIR.join("komorebi.sock");
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
ErrorKind::NotFound => {}
_ => {
return Err(error.into());
}
},
};
let mut stream = UnixStream::connect(socket)?;
stream.write_all(bytes)?;
stream.shutdown(Shutdown::Write)?;
f()?;
let mut reader = BufReader::new(stream);
let mut response = String::new();
reader.read_to_string(&mut response)?;
let listener = UnixListener::bind(socket)?;
match listener.accept() {
Ok(incoming) => {
let stream = BufReader::new(incoming.0);
for line in stream.lines() {
println!("{}", line?);
}
Ok(response)
}
Ok(())
}
Err(error) => {
panic!("{}", error);
}
// print_query is a helper that queries komorebi and prints the response.
// panics on error.
fn print_query(bytes: &[u8]) {
match send_query(bytes) {
Ok(response) => println!("{response}"),
Err(error) => panic!("{}", error),
}
}
@@ -1247,7 +1260,10 @@ fn main() -> Result<()> {
let home_dir = dirs::home_dir().expect("could not find home dir");
let config_dir = home_dir.join(".config");
let local_appdata_dir = data_local_dir().expect("could not find localdata dir");
let data_dir = local_appdata_dir.join("komorebi");
std::fs::create_dir_all(&config_dir)?;
std::fs::create_dir_all(data_dir)?;
let komorebi_json = reqwest::blocking::get(
format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/v{version}/komorebi.example.json")
@@ -1351,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));
@@ -1405,6 +1421,20 @@ fn main() -> Result<()> {
println!("If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration\n");
}
}
SubCommand::Configuration => {
let static_config = HOME_DIR.join("komorebi.json");
if static_config.exists() {
println!("{}", static_config.display());
}
}
SubCommand::Whkdrc => {
let whkdrc = WHKD_CONFIG_DIR.join("whkdrc");
if whkdrc.exists() {
println!("{}", whkdrc.display());
}
}
SubCommand::AhkLibrary => {
let library = HOME_DIR.join("komorebic.lib.ahk");
let mut file = OpenOptions::new()
@@ -1438,7 +1468,7 @@ fn main() -> Result<()> {
let color_log = std::env::temp_dir().join("komorebi.log");
let file = TailedFile::new(File::open(color_log)?);
let locked = file.lock();
#[allow(clippy::significant_drop_in_scrutinee)]
#[allow(clippy::significant_drop_in_scrutinee, clippy::lines_filter_map_ok)]
for line in locked.lines().flatten() {
println!("{line}");
}
@@ -1735,7 +1765,7 @@ fn main() -> Result<()> {
let exec = if let Ok(output) = Command::new("where.exe").arg("komorebi.ps1").output() {
let stdout = String::from_utf8(output.stdout)?;
match stdout.trim() {
stdout if stdout.is_empty() => None,
"" => None,
// It's possible that a komorebi.ps1 config will be in %USERPROFILE% - ignore this
stdout if !stdout.contains("scoop") => None,
stdout => {
@@ -1996,15 +2026,13 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
)?;
}
SubCommand::State => {
with_komorebic_socket(|| send_message(&SocketMessage::State.as_bytes()?))?;
print_query(&SocketMessage::State.as_bytes()?);
}
SubCommand::VisibleWindows => {
with_komorebic_socket(|| send_message(&SocketMessage::VisibleWindows.as_bytes()?))?;
print_query(&SocketMessage::VisibleWindows.as_bytes()?);
}
SubCommand::Query(arg) => {
with_komorebic_socket(|| {
send_message(&SocketMessage::Query(arg.state_query).as_bytes()?)
})?;
print_query(&SocketMessage::Query(arg.state_query).as_bytes()?);
}
SubCommand::RestoreWindows => {
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
@@ -2095,11 +2123,17 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
SubCommand::LoadResize(arg) => {
send_message(&SocketMessage::Load(resolve_home_path(arg.path)?).as_bytes()?)?;
}
SubCommand::Subscribe(arg) => {
send_message(&SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
SubCommand::SubscribeSocket(arg) => {
send_message(&SocketMessage::AddSubscriberSocket(arg.socket).as_bytes()?)?;
}
SubCommand::Unsubscribe(arg) => {
send_message(&SocketMessage::RemoveSubscriber(arg.named_pipe).as_bytes()?)?;
SubCommand::UnsubscribeSocket(arg) => {
send_message(&SocketMessage::RemoveSubscriberSocket(arg.socket).as_bytes()?)?;
}
SubCommand::SubscribePipe(arg) => {
send_message(&SocketMessage::AddSubscriberPipe(arg.named_pipe).as_bytes()?)?;
}
SubCommand::UnsubscribePipe(arg) => {
send_message(&SocketMessage::RemoveSubscriberPipe(arg.named_pipe).as_bytes()?)?;
}
SubCommand::ToggleMouseFollowsFocus => {
send_message(&SocketMessage::ToggleMouseFollowsFocus.as_bytes()?)?;
@@ -2235,23 +2269,19 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
);
}
SubCommand::ApplicationSpecificConfigurationSchema => {
with_komorebic_socket(|| {
send_message(&SocketMessage::ApplicationSpecificConfigurationSchema.as_bytes()?)
})?;
print_query(&SocketMessage::ApplicationSpecificConfigurationSchema.as_bytes()?);
}
SubCommand::NotificationSchema => {
with_komorebic_socket(|| send_message(&SocketMessage::NotificationSchema.as_bytes()?))?;
print_query(&SocketMessage::NotificationSchema.as_bytes()?);
}
SubCommand::SocketSchema => {
with_komorebic_socket(|| send_message(&SocketMessage::SocketSchema.as_bytes()?))?;
print_query(&SocketMessage::SocketSchema.as_bytes()?);
}
SubCommand::StaticConfigSchema => {
with_komorebic_socket(|| send_message(&SocketMessage::StaticConfigSchema.as_bytes()?))?;
print_query(&SocketMessage::StaticConfigSchema.as_bytes()?);
}
SubCommand::GenerateStaticConfig => {
with_komorebic_socket(|| {
send_message(&SocketMessage::GenerateStaticConfig.as_bytes()?)
})?;
print_query(&SocketMessage::GenerateStaticConfig.as_bytes()?);
}
}

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

@@ -19,107 +19,129 @@
"properties": {
"monocle": {
"description": "Border colour when the container is in monocle mode",
"type": "object",
"required": [
"b",
"g",
"r"
],
"properties": {
"b": {
"description": "Blue",
"type": "integer",
"format": "uint32",
"minimum": 0.0
"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
}
}
},
"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"
}
}
]
},
"single": {
"description": "Border colour when the container contains a single window",
"type": "object",
"required": [
"b",
"g",
"r"
],
"properties": {
"b": {
"description": "Blue",
"type": "integer",
"format": "uint32",
"minimum": 0.0
"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
}
}
},
"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"
}
}
]
},
"stack": {
"description": "Border colour when the container contains multiple windows",
"type": "object",
"required": [
"b",
"g",
"r"
],
"properties": {
"b": {
"description": "Blue",
"type": "integer",
"format": "uint32",
"minimum": 0.0
"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
}
}
},
"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"
}
}
]
}
}
},
"active_window_border_offset": {
"description": "Offset of the active window border (default: None)",
"type": "integer",
"format": "int32"
},
"active_window_border_width": {
"description": "Width of the active window border (default: 20)",
"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",
@@ -138,7 +160,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -155,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": [
@@ -209,7 +237,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -278,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",
@@ -327,7 +356,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -362,7 +392,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -498,7 +529,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -524,7 +556,8 @@
"Rows",
"VerticalStack",
"HorizontalStack",
"UltrawideVerticalStack"
"UltrawideVerticalStack",
"Grid"
]
},
"layout_rules": {
@@ -538,7 +571,8 @@
"Rows",
"VerticalStack",
"HorizontalStack",
"UltrawideVerticalStack"
"UltrawideVerticalStack",
"Grid"
]
}
},
@@ -569,7 +603,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -614,7 +649,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {
@@ -636,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",
@@ -654,7 +827,8 @@
"enum": [
"Exe",
"Class",
"Title"
"Title",
"Path"
]
},
"matching_strategy": {