Compare commits

...

109 Commits

Author SHA1 Message Date
LGUG2Z
c62ddb3c42 chore(release): v0.1.7 2022-01-17 08:19:09 -08:00
LGUG2Z
87e8eb48a6 fix(wm): update state for any new float rules
This update ensures that whenever a new float rule is added, the focused
workspaces on all monitors will be checked to see if there are any
currently managed windows which match that rule. If so, the matching
window(s) will be removed from the workspace and the workspace will be
updated.

Matching windows on non-focused workspaces will not be removed, as these
windows may be hidden, and removing them could result in these windows
being inaccessible, requiring them to be killed before they can be
relaunched

fix #93
2022-01-17 08:13:56 -08:00
LGUG2Z
5f1356b3e2 docs(readme): update link to aik2's post 2022-01-14 08:17:09 -08:00
LGUG2Z
00df672352 chore(deps): bump heck from 0.3.3 to 0.4.0 2022-01-13 14:13:12 -08:00
dependabot[bot]
2b83ff8148 chore(deps): bump sysinfo from 0.21.2 to 0.22.4
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.21.2 to 0.22.4.
- [Release notes](https://github.com/GuillaumeGomez/sysinfo/releases)
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-13 14:09:12 -08:00
LGUG2Z
749e247d85 chore(deps): bump windows-rs to 0.30 2022-01-13 14:08:43 -08:00
LGUG2Z
e70086b681 docs(readme): add links to articles, blogs etc 2022-01-11 07:37:39 -08:00
LGUG2Z
39685dd615 feat(wm): add cmd to move ws to other monitors
This commit adds a new komorebic command to move the entire focused
workspace and all managed windows and containers to a target monitor
index. Windows that have been excluded from management using various
rules will not be moved as they are not tracked in the window manager
state.

resolve #88
2022-01-07 08:52:18 -08:00
LGUG2Z
228ef78d7f chore(deps): bump clap to 3.0.0 2021-12-31 15:19:14 -08:00
LGUG2Z
e2ae9b1207 chore(deps): bump clap to latest rc 2021-12-31 08:33:29 -08:00
LGUG2Z
5e3f1cbb44 fix(wm): send state errors to komorebic
This commit ensures that errors are sent to komorebic in response to the
state command if they occur, so that komorebic is not left hanging
indefinitely waiting for a successful response that will never come.
2021-12-06 16:43:22 -08:00
LGUG2Z
9fd4dbf044 fix(wm): handle empty vd reg values gracefully
This commit fixes a regression introduced in
85fe20ebba, where running komorebi before
creating and interacting with virtual desktops via the task view on
Windows 10 would cause komorebi to panic when it could not find the
CurrentVirtualDesktop key in the registry, as it only gets populated
after interacting with virtual desktops via the task view in a new
session.
2021-12-03 22:24:50 -08:00
LGUG2Z
9be248bc03 feat(wm): add cmd to focus ws on target monitor
This commit adds a new command which allows the focusing of workspaces
on monitors other than the currently focused monitor by specifying a
monitor index.

Sending this command to komorebi will make the target monitor index the
currently focused monitor.

resolve #85
2021-12-03 14:52:14 -08:00
LGUG2Z
85fe20ebba refactor(wm): validate virtual desktops via reg
This commit refactors the validations that ensure that only commands and
events originating on the same virtual desktop that komorebi was started
on are managed.

This was previously handled by the winvd crate which relied on
undocumented APIs that broke as of Windows 11. This method, while not
very elegant, seems like the best solution for now.

In short, komorebi checks the registry (which has different paths on
Win10 and Win11...) to keep track of the current virtual desktop id.

This is problematic because we just end up comparing byte arrays, and
there is no meaningful representation of the ids that are being
compared, not even a GUID.  Nevertheless, it works and it ensures that
komorebi is limited to operating on a single virtual desktop.

resolve #77
2021-12-02 18:44:44 -08:00
LGUG2Z
409d374b72 fix(wm): enforce virtual desktop validation
This commit fixes a regression introduced in
2e86b607b2
which broke virtual desktop id validation on Windows 10.
2021-12-02 15:59:09 -08:00
dependabot[bot]
1fb0a7cd6e chore(deps): bump tracing-subscriber from 0.3.1 to 0.3.3
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.1 to 0.3.3.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.1...tracing-subscriber-0.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 11:51:46 -08:00
dependabot[bot]
d0e46515c5 chore(deps): bump strum from 0.21.0 to 0.23.0
Bumps [strum](https://github.com/Peternator7/strum) from 0.21.0 to 0.23.0.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 11:51:31 -08:00
dependabot[bot]
e01bbd9f74 chore(deps): bump sysinfo from 0.20.5 to 0.21.1
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.20.5 to 0.21.1.
- [Release notes](https://github.com/GuillaumeGomez/sysinfo/releases)
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 09:00:33 -08:00
dependabot[bot]
be53ea2c24 chore(deps): bump syn from 1.0.81 to 1.0.82
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.81 to 1.0.82.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.81...1.0.82)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 09:00:16 -08:00
dependabot[bot]
d49279e888 chore(deps): bump getset from 0.1.1 to 0.1.2
Bumps [getset](https://github.com/Hoverbear/getset) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/Hoverbear/getset/releases)
- [Commits](https://github.com/Hoverbear/getset/compare/0.1.1...0.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 09:00:05 -08:00
dependabot[bot]
daa2912945 chore(deps): bump serde_json from 1.0.71 to 1.0.72
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.71 to 1.0.72.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.71...v1.0.72)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 08:59:36 -08:00
dependabot[bot]
2c515d54f7 chore(deps): bump miow from 0.3.7 to 0.4.0
Bumps [miow](https://github.com/yoshuawuyts/miow) from 0.3.7 to 0.4.0.
- [Release notes](https://github.com/yoshuawuyts/miow/releases)
- [Changelog](https://github.com/yoshuawuyts/miow/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yoshuawuyts/miow/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 08:59:18 -08:00
LGUG2Z
f9785bef55 feat(wm): add option to either minimize or hide
This commit adds a command to let the user decide if they want windows
to be hidden with SW_HIDE or minimized with SW_MINIMIZE when workspaces
are changed or window container stacks are cycled.

After a modest amount of local testing, SW_MINIMIZE does not appear to
introduce any regressions, and given that alt-tabbing is a common
workflow on Windows, it makes sense to have minimizing be the default
setting to ease the onboarding experience for new users.

resolve #72
2021-11-20 15:18:45 -08:00
LGUG2Z
0519ebddbf fix(wm): ignore mstsc.exe helper classes on w11
This commit adds OPContainerClass and IHWindowClass to the
FLOAT_IDENTIFIERS global vec, to ignore by default the extra invisible
input and output handling windows created by mstsc.exe when WSL is
launched on Windows 11.

fix #74
2021-11-20 08:32:43 -08:00
LGUG2Z
b1ca0a3e3c feat(wm): enforce last known layout on unpause
This commit ensures that the last known layout is restored on a
workspace when a command to unpause has been received.
2021-11-19 16:34:38 -08:00
LGUG2Z
84ccfedad4 chore(deps): bump windows-rs from 0.26 to 0.28 2021-11-18 14:09:54 -08:00
LGUG2Z
adcb38fed9 chore(deps): bump windows-rs from 0.25 to 0.26 2021-11-15 12:49:51 -08:00
LGUG2Z
4a19edaab2 feat(wm): impl drag to move in append mode
This commit ensures that when a window is dragged over another window container while
WindowContainerBehaviour::Append is set, the window will be removed from its current
container and appended to the target container instead of swapping the positions of the two
containers, as would be the case for WindowContainerBehaviour::Create.

re #72
2021-11-15 12:42:49 -08:00
LGUG2Z
676b643faf feat(wm): add cmd to toggle new window behaviour
This commit introduces a new command, toggle-new-window-behaviour, which
can be used to toggle how new windows on the screen will be handled.

The default setting is to add a new window in a dedicated container, but
when toggled, new windows will be stacked on top of the currently
focused window container.

This can be useful if you only want to use a certain number of columns,
and when you have enough windows on the screen for them, you can toggle
the new window behaviour to start appending to the existing column
stacks.

This commit also fixes a bug where stacked windows being closed did
cause the next window underneath in the stack to be shown.

re #72
2021-11-13 14:31:02 -08:00
LGUG2Z
7f74640dbd chore(deps): bump windows-rs and serde-json 2021-11-05 15:22:45 -07:00
LGUG2Z
c247426b8e ci(artifacts): include .pdb files in archives
resolve #70
2021-11-03 11:10:32 -07:00
LGUG2Z
4d7ccc5519 feat(wm): allow resize-axis for custom primary col
This commit allows the resize-axis cmd on Axis::Horizontal to operate on
the Primary column of a CustomLayout.

Note that this will only operate on a CustomLayout that has met the
window count threshold to enable the tertiary column. If it has not, the
layout will render as DefaultLayout::Columns, which does not support the
resize-axis cmd.
2021-11-03 10:04:51 -07:00
LGUG2Z
71e28b33e3 feat(wm): add resize-delta cmd
This commit adds a command to set the resize delta used under the hood
by the resize-edge and resize-axis commands. The resize delta defaults
to 50 pixels as was hard-coded previously.
2021-11-02 14:16:29 -07:00
LGUG2Z
40226a2bbd refactor(wm): update cmds & keep compat w/ aliases
This commit updates a number of komorebic subcommand names while
maintaining their old names as aliases in order to preserve backwards
compatibility.

Resize becomes ResizeEdge, to complement ResizeAxis, and all of the
commands for saving and loading BSP resize adjustments (QuickSave,
QuickLoad, Save, Load) are now post-fixed with "Resize" in order to make
it clear that these commands are not related to custom layout saving and
loading.
2021-11-02 14:16:29 -07:00
LGUG2Z
2814349228 feat(wm): add cmd to resize by axis
This commit adds a new command to resize by axis. Resizing is still
limited to the BSP layout. This command is intended to be bound to mouse
wheel up and down events, with different modified keys determining the
axis to operate on.
2021-11-02 14:16:29 -07:00
LGUG2Z
d627a1a771 chore(deps): bump windows-rs from 0.23 to 0.24 2021-11-02 13:24:23 -07:00
LGUG2Z
78683ce7b3 chore(deps): bump clap from 3.0.0-beta.4 to beta.5 2021-11-01 14:56:42 -07:00
LGUG2Z
a1ca4f03c3 chore(deps): bump tracing-subscriber and tracing-appender 2021-11-01 14:45:36 -07:00
dependabot[bot]
147a56c274 chore(deps): bump syn from 1.0.80 to 1.0.81
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.80 to 1.0.81.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.80...1.0.81)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 14:39:13 -07:00
LGUG2Z
127254b7ac feat(wm): disable windows ffm on stop and ctrl-c
This commit disables the native Windows ffm implementation as part of
the stop command and the ctrl-c handler.

resolve #69
2021-11-01 13:59:40 -07:00
LGUG2Z
4e6e2b3aa8 feat(wm): mouse follows focus enable/disable cmd
This commit adds a command to explicitly specify the desired state of
mouse follows focus to complement the previously added toggle command.
2021-10-30 16:05:04 -07:00
LGUG2Z
a55069df48 feat(wm): add mouse follows focus toggle
This commit adds a toggle for the mouse follows focus behaviour that has
been the default for komorebi until now.

resolve #63
2021-10-29 13:41:55 -07:00
LGUG2Z
7fd545ca35 fix(wm): handle cross-monitor drag/move events
This commit ensures that when a window is dragged across a monitor
boundary, the ownership of the window container will be transferred to
the target monitor's currently focused workspace.

In order to achieve this, a new WindowManagerEvent variant has been
added, MoveResizeStart, which will store an optional pending_move_op on
the WindowManager struct. This must be consumed at the beginning of the
handler for MoveResizeEnd.

This is necessary because as soon as the window is dragged across a
monitor boundary, an event is sent (and handled) to update the currently
focused monitor and workspace as the target monitor and workspace, and
we still need to have the information about the original monitor,
workspace and container in order to make comparisons and ultimately
remove the origin container to be able to transfer it.

fix #58
2021-10-29 09:32:36 -07:00
LGUG2Z
14e63292e1 chore(deps): bump windows from 0.22 to 0.23 2021-10-28 14:15:21 -07:00
LGUG2Z
18f34babfa chore(deps): bump windows from 0.21 to 0.22
This commit bumps the version of the windows-rs and deprecates the
bindings crate in favour of using the pre-packaged APIs that are
available as of 0.22.
2021-10-28 09:01:10 -07:00
LGUG2Z
2f7ae6f15f build(just): add justfile for common tasks 2021-10-27 19:21:54 -07:00
LGUG2Z
29a6c39084 feat(subscriptions): embed latest state
This commit embeds the latest window manager state (as returned from
'komorebic.exe state') as part of the event notifications sent to
subscribers.

Separately, WindowManager.update_focused_workspace has been refactored
to allow a failure to set the foreground window to the default desktop
window on an empty workspace to log a warning instead of returning an
error, allowing messages previously impacted by this to run to
conclusion and be surfaced in the event notifications stream.

resolve #56
2021-10-26 18:49:53 -07:00
LGUG2Z
5d0806a8c9 fix(serde): gracefully handle window ser errors
I came across some panics when trying to run the custom serialization of
the Window struct for windows that were in the process of being
destroyed recently.

This commit replaces all of the expect() calls in the Serialize
implementation for Window with calls to serde::ser::Error::custom()
which should fail gracefully without rendering the thread that
previously panicked as useless.

fix #55
2021-10-26 08:48:53 -07:00
LGUG2Z
6c53fd7830 refactor(subscriptions): ensure consistent naming
This commit renames add-subscriber and remove-subscriber to subscribe
and unsubscribe for more semantic consistency in command names, as well
as improving and fixing the cli documentation for these commands.

@denBot's example of how to create named pipes and subscribe to events
has also been added to the readme.
2021-10-25 12:08:59 -07:00
LGUG2Z
6ae59671a2 feat(subscriptions): add and remove subscribers
This commit adds two new commands to add and remove subscribers to
WindowManagerEvent and SocketMessage notifications after they have been
handled by komorebi.

Interprocess communication is achieved using Named Pipes; the
subscribing process must first create the Named Pipe, and then run the
'add-subscriber' command, specifying the pipe name as the argument
(without the pipe filesystem path prepended).

Whenever a pipe is closing or has been closed, komorebi will flag this
as a stale subscription and remove it automatically.

resolve #54
2021-10-25 09:31:59 -07:00
LGUG2Z
f17bfe267e docs(readme): add link to custom layout generator 2021-10-23 08:10:46 -07:00
LGUG2Z
840af215a0 docs(readme): add section about custom layouts
This commit adds some documentation around custom layouts as well as a
YAML example.

The load-layout command has been renamed to load-custom-layout for
consistency.

resolve #50
2021-10-21 16:38:47 -07:00
LGUG2Z
6981d778a9 feat(custom_layout): add yaml file support
This commit adds support for loading custom layouts from yaml files, and
also moves the custom layout loading and validating logic into the
komorebi-core crate.

re #50
2021-10-21 16:30:41 -07:00
LGUG2Z
5d6351f48d feat(custom_layout): add opt width for primary col
This commit adds a ColumnWidth for Column::Primary which can optionally
be given as a percentage of the total work area of a monitor. The
remaining columns will have their widths calculated by dividing the
remaining work area space evenly.

This commit also fixes a bug with the Promote command, which was not
calculating the primary container index of custom layouts properly, and
was also not using this value to update the focused container index at
the end of the promotion handler.

re #50
2021-10-21 16:30:41 -07:00
LGUG2Z
ac0f33f7ed feat(custom_layout): implement navigation
This commit introduces a number of refactors to layouts in general in
order to enable navigation across custom layouts and integrate both
default and custom layouts cleanly into komorebi and komorebic.

Layout has been renamed to DefaultLayout, and Layout is now an enum with
the variants Default and Custom, both of which implement the new traits
Arrangement (for layout calculation) and Direction (for operation
destination calculation).

CustomLayout has been simplified to wrap Vec<Column> and no longer
requires the primary column index to be explicitly defined as this can
be looked up at runtime for any valid CustomLayout.

Given the focus on ultrawide layouts for this feature, I have disabled
(and have not yet written the logic for) vertical column splits in
custom layouts.

Since CustomLayouts will be loaded from a file path, a bunch of
clap-related code generation stuff has been removed from the related
enums and structs.

Layout flipping has not yet been worked on for custom layouts.

When switching between Default and Custom layout variants, the primary
column index and the 0 element are swapped to ensure that the same
window container is always at the focal point of every layout.

Resizing/dragging to resize is in a bit of weird spot at the moment
because the logic is only implemented for DefaultLayout::BSP right now
and nothing else. I think eventually this will need to be extracted to a
Resize trait and implemented on everything.
2021-10-21 16:30:41 -07:00
LGUG2Z
f19bd3032b feat(custom_layout): calculate layouts adaptively
This commit introduces a new Trait, Dimensions, which requires the
implementation of a fn calculate() -> Vec<Rect>, a fn that was
previously limited to the Layout struct.

Dimensions is now implemented both for Layout and the new CustomLayout
struct, the latter being a general adaptive fn which employs a number of
fallbacks to sane defaults when the the layout does not have the minimum
number of required windows on the screen.

The CustomLayout is mainly intended for use on ultra and superultrawide
monitors, and as such uses columns as a basic building block. There are
three Column variants: Primary, Secondary and Tertiary.

The Primary column will typically be somewhere in the middle of the
layout, and will be where a window is placed when promoted using the
komorebic command.

The Secondary column is optional, and can be used one or more times in a
layout, either splitting to accomodate a certain number of windows
horizontally or vertically, or not splitting at all.

The Tertiary window is the final window, which will typically be on the
right of a layout, which must be split either horizontally or vertically
to accomodate as many windows as necessary.

The Tertiary column will only be rendered when the threshold of windows
required to enable it has been met. Until then, the rightmost Primary or
Secondary column will expand to take its place.

If there are less windows than (or a number equal to the) columns
defined in the layout, the windows will be arranged in a basic columnar
layout until the number of windows is greater than the number of columns
defined in the layout.

At this point, although the calculation logic has been completed, work
must be done on the navigation logic before a SocketMessage variant can
be added for loading custom layouts from files.
2021-10-21 16:30:41 -07:00
LGUG2Z
3f3c2815da feature(wm): manage linux gui apps by default
This commit introduces an allow_wsl2_gui override in
Window.should_manage() which ensures that Linux GUI apps being run
through WSLg, VcXsrv or X410 will be automatically tiled.

For now the exes that trigger this override are kept in a static Vec in
the codebase, but this could be made configurable in the future if there
is a specific feature request.

resolve #52, resolve #53
2021-10-21 14:30:58 -07:00
LGUG2Z
7070878f4a chore(rust): migrate to edition 2021
This commit applies 'cargo fix --edition' to safely migrate the project
to Edition 2021 of Rust.

A rustfmt.toml has also be added to enforce the flattening of use
statements when running 'cargo fmt'.
2021-10-21 12:08:10 -07:00
LGUG2Z
d3cb9e07f7 fix(wm): keep multi-window app hwnds when stacking
This commit fixes a boolean logic error with an extra pair of parens to
ensure that apps like Firefox don't end up with their HWNDs reaped by
Workspace.remove_window() when another window is stocked on top of them.

fix #51
2021-10-18 13:44:01 -07:00
LGUG2Z
6f6181625f refactor(layouts): compose row and column fns
This commit extracts independent functions for calculating row and
column layouts in an arbitrary work area. This should be useful in the
future for some ideas I have around custom serializable layouts.
2021-10-15 10:49:04 -07:00
LGUG2Z
80dd07fcde chore(release): v0.1.6 2021-10-15 07:49:55 -07:00
LGUG2Z
09d1d69668 fix(wm): apply container padding in monocle mode
This commit ensures that any configured container padding is also
applied when monocle mode is enabled for containers in any/all layouts.

fix #49
2021-10-14 16:03:56 -07:00
LGUG2Z
786f5e846a feat(wm): add vertical & horizontal stack layouts
This commit ports the CenterMain, MainAndVertStack, and
MainAndHorizontalStack layouts from LeftWM to komorebi as
UltrawideVerticalStack, VerticalStack and HorizontalStack.

These layouts are fixed-size layouts, meaning that individual containers
cannot be resized. The VerticalStack and UltrawideVerticalStack layouts
support horizontal flipping, whereas the HorizontalStack layout supports
vertical flipping.

resolve #48
2021-10-14 11:28:44 -07:00
LGUG2Z
65bc1a966e feat(wm): add cmd to specify work area offsets
This commit adds a new komorebic command to specify offsets for work
areas to be applied across all monitors. The areas covered by these
offsets will be excluded from the tiling area, and can be used for
custom task bars, Rainmeter desktop widgets etc.

When setting an offset at the top, the same offset will need to be
applied to the bottom to ensure that the tiling area is not pushed off
of the screen, but this is not necessary when applying an offset to the
bottom as the top of the work area will never go lower than 0.

resolve #46
2021-10-14 11:08:25 -07:00
LGUG2Z
ddafe599a2 feat(wm): cycle through monitors and workspaces
This commit adds commands to navigate monitors and workspaces using
cycle directions.

resolve #47
2021-10-12 07:44:47 -07:00
LGUG2Z
7ed6df511f feat(wm): allow focusing and moving by cycle direction
This commit adds focusing and moving window containers using cycle
directions when the layout has not been flipped on any axis.

This naive implementation simply increments or decrements the index
number in the desired direction and does not accomodate for axis
flipping.

When the current index number is either at the beginning or the end of
the collection, further operations will loop around.

Ideally I would like an implementation which works coherently on any
LayoutFlip state, but this can be implemented at a later date if
specifically requested in the future.

re #47
2021-10-12 07:44:33 -07:00
dependabot[bot]
f9c4dbd447 chore(deps): bump windows from 0.21.0 to 0.21.1
Bumps [windows](https://github.com/microsoft/windows-rs) from 0.21.0 to 0.21.1.
- [Release notes](https://github.com/microsoft/windows-rs/releases)
- [Changelog](https://github.com/microsoft/windows-rs/blob/master/docs/changelog.md)
- [Commits](https://github.com/microsoft/windows-rs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-05 07:15:51 -07:00
dependabot[bot]
b344888b72 chore(deps): bump sysinfo from 0.20.3 to 0.20.4
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.20.3 to 0.20.4.
- [Release notes](https://github.com/GuillaumeGomez/sysinfo/releases)
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-05 07:15:41 -07:00
dependabot[bot]
a62ed682de chore(deps): bump dirs from 3.0.2 to 4.0.0
Bumps [dirs](https://github.com/soc/dirs-rs) from 3.0.2 to 4.0.0.
- [Release notes](https://github.com/soc/dirs-rs/releases)
- [Commits](https://github.com/soc/dirs-rs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-05 07:15:01 -07:00
LGUG2Z
94e9bb8e9e chore(deps): bump windows-rs, syn and instant 2021-09-25 09:13:43 -07:00
LGUG2Z
644f7ee604 chore(release): v0.1.5 2021-09-22 08:43:51 -07:00
LGUG2Z
b9a40924a8 feat(wm): add saving/loading of layouts to file
This commit expands on the autosave/load functionality to allow saving
and loading layouts from any file.

Handling relative paths and paths with ~ on Windows is a little tricky
so I added a helper fn to komorebic to deal with this, ensuring all the
processing happens in komorebic before the messages get sent to komorebi
for processing.

There will still some lingering uses of ContextCompat around the
codebase which I also took the opportunity to clean up and replace with
ok_or_else + anyhow!().

windows-rs is also updated to 0.20.1 in the lockfile.

resolve #41
2021-09-22 08:31:50 -07:00
LGUG2Z
80bcb51f75 feat(wm): add quicksaving/loading of sizes/layouts
This commit adds two new komorebic commands to quicksave and quickload
BSP layouts with custom resize dimensions. The quicksave file is stored
at ${Env:TEMP}/komorebi.quicksave.json, and is a Vec<Option<Rect>>
serialized to JSON.

If a user tries to quickload without a quicksave file being present, an
error will be logged.

At this point there is only one quicksave file which will always be
overwritten whenever the quicksave command is called. Both commands will
only operate on the focused workspace of the focused monitor.

This means that you can quicksave a layout on one workspace, and then
quickload it onto multiple other workspaces (individually) on the same
or other monitors.

If the number of elements in the deserialized Vec is greater than the
number of containers on a workspace, the Vec will be truncated when
Workspace.update is run, and similarly if the number of elements is less
than the number of containers on a workspace, the Vec will be extended
by the difference using None values.

resolve #39
2021-09-21 17:12:18 -07:00
LGUG2Z
e10e11d1de fix(wm): preserve resize dimensions on promotion
Whatever resize dimensions are at the front of the workspace were
previously being thrown away and overwritten with None whenever a
Promote command was being handled.

This commit preserves any resize dimensions that may already be there
and restores them after the container promotion has been completed.

fix #40
2021-09-21 10:07:15 -07:00
LGUG2Z
2807cafdd0 refactor(windows_api): use handle trait from 0.20
The 0.20.0 release of windows-rs includes a Handle trait which provides
ok() and invalid() fns for implementors, including HWND and HANDLE.

This is pretty cool (and also a big breaking change since the release
takes away is_null() at the same time...), so the code in windows_api.rs
has been updated to make use of this by implementing a
ProcessWindowsCrateResult trait with a process() fn.

When implemented for a windows::Result<T>, it will do any required
processing for T, and ensure that windows::Error is converted to an
eyre-compatible Report.

Switching to this means that I have been able to get rid of some of the
hacky error handling for weird behaviours encountered previously. So
far, they don't seem to be presenting again, but I will run with this
build for a couple of days to see if the false-negative errors are
really gone for good with this update.
2021-09-20 18:41:31 -07:00
LGUG2Z
63cf48daa5 fix(wm): ensure idx < len before container removal
fix #38
2021-09-19 11:05:09 -07:00
LGUG2Z
a2b49845ac chore(release): v0.1.4 2021-09-17 08:05:59 -07:00
LGUG2Z
5b923a135c feat(wm): adapt to scaling and resolution changes
This commit expands the reconcile_monitors fn to also update resolution
and work area sizes if they are different from what is stored in the
window manager state.

Another WindowManagerEvent has been added as a polling mechanism for
monitor-related changes (scaling, dpi, resolution etc.), and this will
now also trigger the reconcile_monitors fn in the existing event
pre-processing block.

resolve #36
2021-09-16 14:53:07 -07:00
LGUG2Z
b8a27a93fe feat(ffm): explicitly require flag to enable
Following the discovery that the custom FFM implementation significantly
increases CPU usage, and that the underlying library used to track mouse
events is already as optimised as possible for CPU usage, this commit
makes the enabling of custom FFM explicit via a command line flag when
launching the window manager.

The underlying library does not provide for a way to clean up and
recreate a message loop on demand, which means that once it starts,
there is no way of reclaiming those CPU cycles even when FFM is
disabled.

If a user has not started komorebi with the --ffm flag and tries to
enable or toggle custom FFM, a warning will be shown in the logs and
komorebi will override their selection to operate on the Windows FFM
implementation.

In light of this, the default implementation values for komorebic's FFM
commands have been updated to 'windows'.

This commit also takes the opportunity to allow the state and stop
commands to pass when the window manager is in a paused state.

resolve #33
2021-09-16 09:42:13 -07:00
LGUG2Z
28a641609c fix(wm): update target monitor after send op
This commit ensures that the focused workspace on the target monitor is
updated with the latest layout after it receives a window via the
send-to-monitor command.

resolve #37
2021-09-15 07:18:10 -07:00
LGUG2Z
f1ee5ea194 feat(wm): make invisible borders configurable
Following the changes I witnessed in the invisible window border size
following an OS update, this commit makes the invisible border offset
configurable via a new komorebic command 'invisible-borders'.

When sending a new set of invisible border offset dimensions via
komorebic, a full retile across all monitors will take place after the
new values have been set.

The default values have been set to what is currently correct for my
machine, and will likely be updated again in the same way in the future
if further changes occur in subsequent OS updates.

This commit also updates some dependencies to their latest releases, and
removes from the CI workflow a line that attempts to delete the
rustup-init.exe binary after installation which has been causing builds
to fail.

resolve #35
2021-09-14 21:26:18 -07:00
LGUG2Z
ff53533da0 feat(wm): add cmd to id apps that overflow borders
Applications like Spotify and Discord draw over the default invisible
borders of Windows 10, which means that when komorebi is setting their
positions, the offset is always off by the amount of pixels of the
invisible borders on each side.

This commit makes it possible to identify applications that have
overflowing borders so that they can be handled appropriately by the
window manager.

This commit also takes the opportunity to consolidate the tray and multi
window identifiers into a single vector instead of spreading them across
multiple vectors by identifier type.

resolve #32
2021-09-13 09:03:40 -07:00
LGUG2Z
c4c8bd7d4b feat(wm): reconcile monitor state
When monitors turn on and off, they do not retain their hmonitor id,
therefore this commit introduces an initial attempt to reconcile invalid
and valid hmonitors after monitor changes based on the windows that are
assigned to them.

If a monitor has at least one window, and has been assigned a new
hmonitor id, komorebi will look up the current hmonitor of that window's
hwnd and update Monitor.id in-place.

When reconciling monitors, any monitor marked as invalid will be purged
from the window manager state.

This commit also applies some of the new clippy lints that come along
with the latest nightly release of Rust.

resolve #31
2021-09-13 07:24:09 -07:00
LGUG2Z
e1bd0e9fcb fix(ffm): handle multiple overlay window classes
Explorer windows are made up of multiple different sub-windows which can
make trouble for the ffm implementation if not known about.

This commit pushes up a check to ignore the raise request if the window
are the cursor position is already raised and in the foreground, and
also makes checks against an overlay_classes array to try and look up an
underlying hwnd that should probably be raised by a cursor move.
2021-09-07 08:38:41 -07:00
LGUG2Z
368d41e3e0 fix(ffm): raise when switching focus from anywhere
Previously, when switching focus from unmanaged windows such as the
taskbar and the system tray, komorebi would still recognise its last
known focused window as the currently focused window although that would
not be strictly true, making the ffm functionality stop working until
focus was set to a known window specifically either via a click or an
alt-tab.

This commit fixes that by always also comparing against what the OS has
registered as the current foreground window.

There is another little fix here to reset the pending raise op tracker
whenever ffm is toggled or disabled in the event that it ever gets
stuck.
2021-09-07 08:38:41 -07:00
LGUG2Z
2b7c51b87b refactor(ffm): add selection of ffm implementation
This commit adds an optional flag to allow users to select the focus
follows mouse implementation that they wish to use (komorebi or
windows). The flag defaults to komorebi.

The ahk-derive crate has been updated to enable the generation of
wrappers fns that require flags.

I pushed the ffm check up to listen_for_movements() so that we don't
even try to listen to the next event from the message loop unless
komorebi-flavoured ffm is enabled.

re #7
2021-09-07 08:38:41 -07:00
LGUG2Z
ce3c742e09 feat(ffm): add custom ffm/autoraise implementation
This commit implements an initial attempt at a custom focus follows
mouse and autoraise implementation which only responds to hwnds managed
by komorebi.

I was browsing GitHub and came across the winput crate which has a clean
API for tracking both mouse movements and button presses, which seems to
be just enough to get this functionality working.

Once again, Chromium and Electron are the bane of every platform they
run on and Windows is no exception, so I've had to add a hack to work
around the legacy Chrome windows that get drawn on top of Electron apps
with the Chrome_RenderWidgetHostHWND class.

It is fairly naive; it just looks up an alternative (and hopefully
correct) hwnd based on the exe name, but this will no doubt be fragile
when it comes to applications that have multiple windows spawned from
the same exe.

For now I've opted to keep the same komorebic commands for enabling,
disabling and toggling focus-follows-mouse, in order to preserve
backwards compat, but those commands will now enable and disable this
custom implementation instead of the native Windows X-Mouse
implementation.

Perhaps in the future the specific implementation to target could be
specified through the use of an optional flag.

re #7
2021-09-07 08:38:41 -07:00
LGUG2Z
2a4e6fa6da feat(wm): get monitor idx from cursor on ws switch
If a user wants to switch workspace on a secondary monitor which
contains no windows, komorebi doesn't register the monitor as focused
unless the corresponding komorebic command to switch monitor focus is
called explicitly. This generally works fine for users with a
keyboard-heavy workflow.

This commit changes the workspace switching behaviour to look up the
current monitor based on the cursor position just before the switch
takes place, so that the behaviour is still intuitive when trying to
change the monitor focus via the mouse.

re #30
2021-09-07 08:33:53 -07:00
LGUG2Z
2d19109fb6 feat(wm): allow direct querying of focused objects
This commit adds a new query command to komorebic, which allows for the
current focused monitor, workspace, container and window indices to be
queried directly without having to use jq run lookups on the entire
output of the state command.

resolve #24
2021-09-02 12:58:14 -07:00
LGUG2Z
a9a0ecd49d chore(deps): bump cc and object crates 2021-09-01 08:12:52 -07:00
dependabot[bot]
5ec2b80c3a chore(deps): bump proc-macro2 from 1.0.28 to 1.0.29
Bumps [proc-macro2](https://github.com/alexcrichton/proc-macro2) from 1.0.28 to 1.0.29.
- [Release notes](https://github.com/alexcrichton/proc-macro2/releases)
- [Commits](https://github.com/alexcrichton/proc-macro2/compare/1.0.28...1.0.29)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:34:00 -07:00
dependabot[bot]
4cc059ff1d chore(deps): bump sysinfo from 0.20.1 to 0.20.2
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.20.1 to 0.20.2.
- [Release notes](https://github.com/GuillaumeGomez/sysinfo/releases)
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:28:03 -07:00
dependabot[bot]
d4d7e2b694 chore(deps): bump parking_lot from 0.11.1 to 0.11.2
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.11.1 to 0.11.2.
- [Release notes](https://github.com/Amanieu/parking_lot/releases)
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/0.11.1...0.11.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:25:07 -07:00
dependabot[bot]
84752c4338 chore(deps): bump serde_json from 1.0.66 to 1.0.67
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.66 to 1.0.67.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.66...v1.0.67)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:24:35 -07:00
dependabot[bot]
ffa0786178 chore(deps): bump serde from 1.0.129 to 1.0.130
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.129 to 1.0.130.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.129...v1.0.130)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:24:13 -07:00
LGUG2Z
752dde2288 docs(readme): add link to zh translation on wiki
Adding a section under ##About for links to translations of the document
in other languages, starting with @crosstyan's Chinese translation.

resolve #21
2021-08-29 07:30:25 -07:00
LGUG2Z
dc771a104d build(cargo): update dependencies 2021-08-26 09:58:18 -07:00
LGUG2Z
e5a7c140ff refactor(macros): add trailing commas 2021-08-26 09:53:15 -07:00
LGUG2Z
61b231be28 docs(readme): fix some bad copypasta 2021-08-24 11:05:33 -07:00
LGUG2Z
cab8b4ad52 chore(release): v0.1.3 2021-08-24 08:28:44 -07:00
LGUG2Z
05777c34b9 fix(wm): ensure removal of max + monocle windows
Previously, the implementation of maximized and monocle windows assumed
that the only valid state for them to transition to would be to restore
them to the index that they were maximized/monocle-d from in their host
workspace.

This is not exclusively the case as it is also possible for them to be
closed when they are in a maximized or monocle state.

This commit updates the Workspace.remove_window() fn to also look for
the hwnd to be removed in the monocle container and maximized window, if
they exist.

fix #19
2021-08-24 07:21:13 -07:00
LGUG2Z
5094001862 feat(wm): add send-to-workspace/monitor cmds
This commit adds two commands to allow the user to send the currently
focused container to a different workspace or monitor as a background
operation, without following the moved container to the destination
workspace or monitor.

resolve #20
2021-08-24 06:52:56 -07:00
LGUG2Z
bc08e177a1 fix(komorebic): add missing help annotations 2021-08-23 15:14:16 -07:00
LGUG2Z
87fe718754 feat(wm): add toggle-focus-follows-mouse cmd
Decided there should be a quick way to toggle the native ffm
functionality, it gets especially annoying when trying to click drop
downs from the system tray etc.

re #7
2021-08-23 14:08:40 -07:00
LGUG2Z
fb4fe4d9c3 refactor(derive-ahk): enforce no_implicit_prelude
Starting to implement the feedback I got from this post on Reddit
https://old.reddit.com/r/rust/comments/pa2997/code_review_request_first_derive_macro/.
2021-08-23 11:16:58 -07:00
LGUG2Z
b61b03b1c9 refactor(eyre): handle options with combinators
This commit removes the unnecessary eyre dependency and instead uses the
relevant imports from color-eyre.

Additionally, after reading the eyre readme a little more closely, I
have switched out .compat() for the ok_or() combinator function as
suggested here: https://github.com/yaahc/eyre#compatibility-with-anyhow.
2021-08-23 09:52:13 -07:00
LGUG2Z
a02cd699a0 refactor(derive-ahk): push up generation logic
This commit pushes as much of the generation logic as possible to the
derive-ahk crate, so that when it is used in komorebic, we only need to
do an as_bytes() call to prepare it for being written to a file.

Besides that, this commit changes the generation command name to
'ahk-library' for clarity, and adds both additional samples and
instructions in the readme file and Scoop post-install hook.
2021-08-23 07:49:37 -07:00
LGUG2Z
2c876701d8 feat(ahk): add cmd to generate helper lib
Woke up today and thought this would be a cool way to learn more about
deriving functionality with proc macros.

Hopefully having this wrapper/helper library will make first time
configuration for new users easier.
2021-08-22 18:54:44 -07:00
LGUG2Z
c42739591f build(windows-rs): upgrade to 0.19.0 2021-08-22 07:19:34 -07:00
43 changed files with 5866 additions and 1880 deletions

View File

@@ -66,7 +66,6 @@ jobs:
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
del rustup-init.exe
shell: powershell
- name: Ensure stable toolchain is up to date
run: rustup update stable
@@ -84,6 +83,8 @@ jobs:
path: |
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
retention-days: 7
- name: Generate changelog
if: startsWith(github.ref, 'refs/tags/')

View File

@@ -55,5 +55,6 @@ scoop:
pre_install:
- if (Get-Process -Name komorebi -ErrorAction SilentlyContinue) { komorebic stop }
post_install:
- Write-Host "Run 'cp $original_dir\komorebi.sample.ahk $Env:UserProfile\komorebi.ahk' to get started with the sample configuration"
- Write-Host "Once you have a configuration file in place, you can run 'komorebic start' to start the window manager"
- Write-Host "`nRun 'cp $original_dir\komorebi.sample.ahk $Env:UserProfile\komorebi.ahk' to get started with the sample configuration"
- Write-Host "`nRun 'komorebic ahk-library' if you would like to generate an AHK helper library to use in your configuration"
- Write-Host "`nOnce you have a configuration file in place, you can run 'komorebic start' to start the window manager"

563
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[workspace]
members = [
"bindings",
"derive-ahk",
"komorebi",
"komorebi-core",
"komorebic"

309
README.md
View File

@@ -14,6 +14,19 @@ _komorebi_ allows you to control application windows, virtual workspaces and dis
used with third-party software such as [AutoHotKey](https://github.com/Lexikos/AutoHotkey_L) to set user-defined
keyboard shortcuts.
Translations of this document can be found in the project wiki:
- [komorebi 中文用户指南](https://github.com/LGUG2Z/komorebi/wiki/README-zh) (by [@crosstyan](https://github.com/crosstyan))
There is a [Discord server](https://discord.gg/vzBmPm6RkQ) available for _komorebi_-related discussion, help,
troubleshooting etc. If you have any specific feature requests or bugs to report, please create an issue in this
repository.
Articles, blog posts, demos, and videos about _komorebi_ can be added to this list by PR:
- [Moving to Windows from Linux Pt 1](https://kvwu.io/posts/moving-to-windows/)
- [Windows 下的现代化平铺窗口管理器 komorebi](https://zhuanlan.zhihu.com/p/455064481)
## Description
_komorebi_ only responds to [WinEvents](https://docs.microsoft.com/en-us/windows/win32/winauto/event-constants) and the
@@ -160,6 +173,109 @@ komorebic.exe identify-tray-application exe Discord.exe
# komorebic.exe identify-tray-application title [TITLE]
```
#### Focus Follows Mouse
`komorebi` supports two focus-follows-mouse implementations; the native Windows Xmouse implementation, which treats the
desktop, the task bar, and the system tray as windows and switches focus to them eagerly, and a custom `komorebi`
implementation, which only considers windows managed by `komorebi` as valid targets to switch focus to when moving the
mouse.
To enable the `komorebi` implementation you must start the process with the `--ffm` flag to explicitly enable the feature.
This is because the mouse tracking required for this feature significantly increases the CPU usage of the process (on my
machine, it jumps from <1% to ~4~), and this CPU increase persists regardless of whether focus-follows-mouse is enabled
or disabled at any given time via `komorebic`'s configuration commands.
When calling any of the `komorebic` commands related to focus-follows-mouse functionality, the `windows`
implementation will be chosen as the default implementation. You can optionally specify the `komorebi` implementation by
passing it as an argument to the `--implementation` flag:
```powershell
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
```
#### Mouse Follows Focus
By default, the mouse will move to the center of the window when the focus is changed in a given direction. This
behaviour is know is 'mouse follows focus'. To disable this behaviour across all workspaces, add the following command
to your configuration file:
```ahk
Run, komorebic.exe toggle-mouse-follows-focus, , Hide
```
#### Saving and Loading Resized Layouts
If you create a BSP layout through various resize adjustments that you want to be able to restore easily in the future,
it is possible to "quicksave" that layout to the system's temporary folder and load it later in the same session, or
alternatively, you may save it to a specific file to be loaded again at any point in the future.
```powershell
komorebic.exe quick-save # saves the focused workspace to $Env:TEMP\komorebi.quicksave.json
komorebic.exe quick-load # loads $Env:TEMP\komorebi.quicksave.json on the focused workspace
komorebic.exe save ~/layouts/primary.json # saves the focused workspace to $Env:USERPROFILE\layouts\primary.json
komorebic.exe load ~/layouts/secondary.json # loads $Env:USERPROFILE\layouts\secondary.json on the focused workspace
```
These layouts can be applied to arbitrary collections of windows on any workspace, as they only track the layout
dimensions and are not coupled to the applications that were running at the time of saving.
When layouts that expect more or less windows than the number currently on the focused workspace are loaded, `komorebi`
will automatically reconcile the difference.
#### Creating and Loading Custom Layouts
Particularly for users of ultrawide monitors, traditional tiling layouts may not seem like the most efficient use of
screen space. If you feel this is the case with any of the default layouts, you are also welcome to create your own
custom layouts and save them as JSON or YAML.
If you're not comfortable writing the layouts directly in JSON or YAML, you can use
the [komorebi Custom Layout Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to interactively
define a custom layout, and then copy the generated JSON content.
Custom layouts can be loaded on the current workspace or configured for a specific workspace with the following
commands:
```powershell
komorebic.exe load-custom-layout ~/custom.yaml
komorebic.exe workspace-custom-layout 0 0 ~/custom.yaml
```
The fundamental building block of a custom _komorebi_ layout is the Column.
Columns come in three variants:
- **Primary**: This is where your primary focus will be on the screen most of the time. There must be exactly one Primary
Column in any custom layout. Optionally, you can specify the percentage of the screen width that you want the Primary
Column to occupy.
- **Secondary**: This is an optional column that can either be full height of split horizontally into a fixed number of
maximum rows. There can be any number of Secondary Columns in a custom layout.
- **Tertiary**: This is the final column where any remaining windows will be split horizontally into rows as they get added.
If there is only one window on the screen when a custom layout is selected, that window will take up the full work area
of the screen.
If the number of windows is equal to or less than the total number of columns defined in a custom layout, the windows
will be arranged in an equal-width columns.
When the number of windows is greater than the number of columns defined in the custom layout, the windows will begin to
be arranged according to the constraints set on the Primary and Secondary columns of the layout.
Here is an example custom layout that can be used as a starting point for your own:
YAML
```yaml
- column: Secondary
configuration:
Horizontal: 2 # max number of rows,
- column: Primary
configuration:
WidthPercentage: 45 # percentage of screen
- column: Tertiary
configuration: Horizontal
```
## Configuration with `komorebic`
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
@@ -171,51 +287,89 @@ keybindings with. You can run `komorebic.exe <COMMAND> --help` to get a full exp
each command.
```
start Start komorebi.exe as a background process
stop Stop the komorebi.exe process and restore all hidden windows
state Show a JSON representation of the current window manager state
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
focus Change focus to the window in the specified direction
move Move the focused window in the specified direction
stack Stack the focused window in the specified direction
resize Resize the focused window in the specified direction
unstack Unstack the focused window
cycle-stack Cycle the focused stack in the specified cycle direction
move-to-monitor Move the focused window to the specified monitor
move-to-workspace Move the focused window to the specified workspace
focus-monitor Focus the specified monitor
focus-workspace Focus the specified workspace on the focused monitor
new-workspace Create and append a new workspace on the focused monitor
adjust-container-padding Adjust container padding on the focused workspace
adjust-workspace-padding Adjust workspace padding on the focused workspace
change-layout Set the layout on the focused workspace
flip-layout Flip the layout on the focused workspace (BSP only)
promote Promote the focused window to the top of the tree
retile Force the retiling of all managed windows
ensure-workspaces Create at least this many workspaces for the specified monitor
container-padding Set the container padding for the specified workspace
workspace-padding Set the workspace padding for the specified workspace
workspace-layout Set the layout for the specified workspace
workspace-tiling Enable or disable window tiling for the specified workspace
workspace-name Set the workspace name for the specified workspace
toggle-pause Toggle the window manager on and off across all monitors
toggle-tiling Toggle window tiling on the focused workspace
toggle-float Toggle floating mode for the focused window
toggle-monocle Toggle monocle mode for the focused container
toggle-maximize Toggle native maximization for the focused window
restore-windows Restore all hidden windows (debugging command)
manage Force komorebi to manage the focused window
unmanage Unmanage a window that was forcibly managed
reload-configuration Reload ~/komorebi.ahk (if it exists)
watch-configuration Toggle the automatic reloading of ~/komorebi.ahk (if it exists)
float-rule Add a rule to always float the specified application
manage-rule Add a rule to always manage the specified application
workspace-rule Add a rule to associate an application with a workspace
identify-tray-application Identify an application that closes to the system tray
focus-follows-mouse Enable or disable focus follows mouse for the operating system
help Print this message or the help of the given subcommand(s)
start Start komorebi.exe as a background process
stop Stop the komorebi.exe process and restore all hidden windows
state Show a JSON representation of the current window manager state
query Query the current window manager state
subscribe Subscribe to komorebi events
unsubscribe Unsubscribe from komorebi events
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
quick-save-resize Quicksave the current resize layout dimensions
quick-load-resize Load the last quicksaved resize layout dimensions
save-resize Save the current resize layout dimensions to a file
load-resize Load the resize layout dimensions from a file
focus Change focus to the window in the specified direction
move Move the focused window in the specified direction
cycle-focus Change focus to the window in the specified cycle direction
cycle-move Move the focused window in the specified cycle direction
stack Stack the focused window in the specified direction
resize-edge Resize the focused window in the specified direction
resize-axis Resize the focused window or primary column along the specified axis
unstack Unstack the focused window
cycle-stack Cycle the focused stack in the specified cycle direction
move-to-monitor Move the focused window to the specified monitor
move-to-workspace Move the focused window to the specified workspace
send-to-monitor Send the focused window to the specified monitor
send-to-workspace Send the focused window to the specified workspace
focus-monitor Focus the specified monitor
focus-workspace Focus the specified workspace on the focused monitor
focus-monitor-workspace Focus the specified workspace on the target monitor
cycle-monitor Focus the monitor in the given cycle direction
cycle-workspace Focus the workspace in the given cycle direction
move-workspace-to-monitor Move the focused workspace to the specified monitor
new-workspace Create and append a new workspace on the focused monitor
resize-delta Set the resize delta (used by resize-edge and resize-axis)
invisible-borders Set the invisible border dimensions around each window
work-area-offset Set offsets to exclude parts of the work area from tiling
adjust-container-padding Adjust container padding on the focused workspace
adjust-workspace-padding Adjust workspace padding on the focused workspace
change-layout Set the layout on the focused workspace
load-custom-layout Load a custom layout from file for the focused workspace
flip-layout Flip the layout on the focused workspace (BSP only)
promote Promote the focused window to the top of the tree
retile Force the retiling of all managed windows
ensure-workspaces Create at least this many workspaces for the specified monitor
container-padding Set the container padding for the specified workspace
workspace-padding Set the workspace padding for the specified workspace
workspace-layout Set the layout for the specified workspace
workspace-custom-layout Set a custom layout for the specified workspace
workspace-tiling Enable or disable window tiling for the specified workspace
workspace-name Set the workspace name for the specified workspace
toggle-window-container-behaviour Toggle the behaviour for new windows (stacking or dynamic tiling)
toggle-pause Toggle window tiling on the focused workspace
toggle-tiling Toggle window tiling on the focused workspace
toggle-float Toggle floating mode for the focused window
toggle-monocle Toggle monocle mode for the focused container
toggle-maximize Toggle native maximization for the focused window
restore-windows Restore all hidden windows (debugging command)
manage Force komorebi to manage the focused window
unmanage Unmanage a window that was forcibly managed
reload-configuration Reload ~/komorebi.ahk (if it exists)
watch-configuration Enable or disable watching of ~/komorebi.ahk (if it exists)
window-hiding-behaviour Set the window behaviour when switching workspaces / cycling stacks
float-rule Add a rule to always float the specified application
manage-rule Add a rule to always manage the specified application
workspace-rule Add a rule to associate an application with a workspace
identify-tray-application Identify an application that closes to the system tray
identify-border-overflow Identify an application that has overflowing borders
focus-follows-mouse Enable or disable focus follows mouse for the operating system
toggle-focus-follows-mouse Toggle focus follows mouse for the operating system
mouse-follows-focus Enable or disable mouse follows focus on all workspaces
toggle-mouse-follows-focus Toggle mouse follows focus on all workspaces
ahk-library Generate a library of AutoHotKey helper functions
help Print this message or the help of the given subcommand(s)
```
### AutoHotKey Helper Library for `komorebic`
Additionally, you may run `komorebic.exe ahk-library` to
generate [a helper library for AutoHotKey](komorebic.lib.sample.ahk) which wraps every `komorebic` command in a native
AHK function.
If you include the generated library at the top of your `~/komorebi.ahk` configuration file, you will be able to call
any of the functions that it contains. A sample AHK script that shows how this library can be
used [is available here](komorebi.sample.with.lib.ahk).
## Features
- [x] Multi-monitor
@@ -224,31 +378,51 @@ help Print this message or the help of the given subcomm
- [x] Cycle through stacked windows
- [x] Change focused window by direction
- [x] Move focused window container in direction
- [x] Move focused window container to monitor
- [x] Move focused window container to workspace
- [x] Move focused window container to monitor and follow
- [x] Move focused window container to workspace follow
- [x] Send focused window container to monitor
- [x] Send focused window container to workspace
- [x] Move focused workspace to monitor
- [x] Mouse follows focused container
- [x] Resize window container in direction
- [x] Resize window container on axis
- [x] Set custom resize delta
- [ ] Resize child window containers by split ratio
- [x] Quicksave and quickload layouts with resize dimensions
- [x] Save and load layouts with resize dimensions to/from specific files
- [x] Mouse drag to swap window container position
- [x] Mouse drag to resize window container
- [x] Configurable workspace and container gaps
- [x] BSP tree layout
- [x] BSP tree layout (`bsp`)
- [x] Flip BSP tree layout horizontally or vertically
- [x] Equal-width, max-height column layout
- [x] Equal-width, max-height column layout (`columns`)
- [x] Equal-height, max-width row layout (`rows`)
- [x] Main half-height window with vertical stack layout (`horizontal-stack`)
- [x] Main half-width window with horizontal stack layout (`vertical-stack`)
- [x] 2x Main window (half and quarter-width) with horizontal stack layout (`ultrawide-vertical-stack`)
- [x] Load custom layouts from JSON and YAML representations
- [x] Floating rules based on exe name, window title and class
- [x] Workspace rules based on exe name and window class
- [x] Additional manage rules based on exe name and window class
- [x] Identify applications which overflow their borders by exe name and class
- [x] Identify 'close/minimize to tray' applications by exe name and class
- [x] Configure work area offsets to preserve space for custom taskbars
- [x] Configure and compensate for the size of Windows 10's invisible borders
- [x] Toggle floating windows
- [x] Toggle monocle window
- [x] Toggle native maximization
- [x] Toggle focus follows mouse
- [x] Toggle mouse follows focus
- [x] Toggle Xmouse/Windows focus follows mouse implementation
- [x] Toggle Komorebi focus follows mouse implementation (desktop and system tray-aware)
- [x] Toggle automatic tiling
- [x] Pause all window management
- [x] Load configuration on startup
- [x] Manually reload configuration
- [x] Watch configuration for changes
- [x] Helper library for AutoHotKey
- [x] View window manager state
- [x] Query window manager state
- [x] Subscribe to event and message notifications
## Development
@@ -297,8 +471,7 @@ process and the log will appear frozen.
If you believe you have encountered a deadlock, you can compile `komorebi` with `--features deadlock_detection` and try
reproducing the deadlock again. This will check for deadlocks every 5 seconds in the background, and if a deadlock is
found, information about it will appear in the log which can be shared when opening an issu which can be shared when
opening an issue.
found, information about it will appear in the log which can be shared when opening an issue.
## Window Manager State and Integrations
@@ -307,3 +480,39 @@ representation of the `State` struct, which includes the current state of `Windo
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).
## Window Manager Event Subscriptions
It is also 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>
```
Note that you do not have to include the full path of the named pipe, just the name.
If the named pipe exists, `komorebi` will start pushing JSON data of successfully handled events and messages:
```json lines
{"event":{"type":"AddSubscriber","content":"yasb"},"state":{}}
{"event":{"type":"FocusWindow","content":"Left"},"state":{}}
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":131444,"title":"komorebi README.md","exe":"idea64.exe","class":"SunAwtFrame","rect":{"left":13,"top":60,"right":1520,"bottom":1655}}]},"state":{}}
{"event":{"type":"MonitorPoll","content":["ObjectCreate",{"hwnd":5572450,"title":"OLEChannelWnd","exe":"explorer.exe","class":"OleMainThreadWndClass","rect":{"left":0,"top":0,"right":0,"bottom":0}}]},"state":{}}
{"event":{"type":"FocusWindow","content":"Right"},"state":{}}
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{}}
{"event":{"type":"FocusWindow","content":"Down"},"state":{}}
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":329264,"title":"den — Mozilla Firefox","exe":"firefox.exe","class":"MozillaWindowClass","rect":{"left":1539,"top":894,"right":1520,"bottom":821}}]},"state":{}}
{"event":{"type":"FocusWindow","content":"Up"},"state":{}}
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{}}
```
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
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).

View File

@@ -1,26 +0,0 @@
fn main() {
windows::build!(
Windows::Win32::Foundation::RECT,
Windows::Win32::Foundation::POINT,
Windows::Win32::Foundation::BOOL,
Windows::Win32::Foundation::PWSTR,
Windows::Win32::Foundation::HWND,
Windows::Win32::Foundation::LPARAM,
// error: `Windows.Win32.Graphics.Dwm.DWMWA_CLOAKED` not found in metadata
Windows::Win32::Graphics::Dwm::*,
// error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata
Windows::Win32::Graphics::Gdi::*,
Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS,
Windows::Win32::System::Threading::PROCESS_NAME_FORMAT,
Windows::Win32::System::Threading::OpenProcess,
Windows::Win32::System::Threading::QueryFullProcessImageNameW,
Windows::Win32::System::Threading::GetCurrentThreadId,
Windows::Win32::System::Threading::AttachThreadInput,
Windows::Win32::System::Threading::GetCurrentProcessId,
Windows::Win32::UI::KeyboardAndMouseInput::SetFocus,
Windows::Win32::UI::Accessibility::SetWinEventHook,
Windows::Win32::UI::Accessibility::HWINEVENTHOOK,
// error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata
Windows::Win32::UI::WindowsAndMessaging::*,
);
}

View File

@@ -1 +0,0 @@
::windows::include_bindings!();

View File

@@ -1,13 +1,14 @@
[package]
name = "bindings"
name = "derive-ahk"
version = "0.1.0"
authors = ["Jade Iqbal"]
edition = "2018"
edition = "2021"
[lib]
proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
windows = "0.18"
[build-dependencies]
windows = "0.18"
proc-macro2 = "1.0"
syn = "1.0"
quote = "1.0"

212
derive-ahk/src/lib.rs Normal file
View File

@@ -0,0 +1,212 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![no_implicit_prelude]
use ::std::clone::Clone;
use ::std::convert::From;
use ::std::convert::Into;
use ::std::iter::Extend;
use ::std::iter::Iterator;
use ::std::matches;
use ::std::option::Option::Some;
use ::std::string::ToString;
use ::std::unreachable;
use ::quote::quote;
use ::syn::parse_macro_input;
use ::syn::Data;
use ::syn::DataEnum;
use ::syn::DeriveInput;
use ::syn::Fields;
use ::syn::FieldsNamed;
use ::syn::FieldsUnnamed;
use ::syn::Meta;
use ::syn::NestedMeta;
#[allow(clippy::too_many_lines)]
#[proc_macro_derive(AhkFunction)]
pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Struct(s) => match s.fields {
Fields::Named(FieldsNamed { named, .. }) => {
let argument_idents = named
.iter()
// Filter out the flags
.filter(|&f| {
let mut include = true;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
if path.is_ident("long") {
include = false;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let argument_idents_clone = argument_idents.clone();
let called_arguments = quote! {#(%#argument_idents_clone%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
let flag_idents = named
.iter()
// Filter only the flags
.filter(|f| {
let mut include = false;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
// Identify them using the --long flag name
if path.is_ident("long") {
include = true;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let has_flags = flag_idents.clone().count() != 0;
if has_flags {
let flag_idents_concat = flag_idents.clone();
let argument_idents_concat = argument_idents.clone();
// Concat the args and flag args if there are flags
let all_arguments =
quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *}
.to_string();
let flag_idents_clone = flag_idents.clone();
let flags = quote! {#(--#flag_idents_clone) *}
.to_string()
.replace("- - ", "--");
let called_flag_arguments = quote! {#(%#flag_idents%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
Run, komorebic.exe {} {} {} {}, , Hide
}}"#,
::std::stringify!(#name),
#all_arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments,
#flags,
#called_flag_arguments
)
}
}
}
} else {
let arguments = quote! {#(#argument_idents), *}.to_string();
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
Run, komorebic.exe {} {}, , Hide
}}"#,
::std::stringify!(#name),
#arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments
)
}
}
}
}
}
_ => unreachable!("only to be used on structs with named fields"),
},
_ => unreachable!("only to be used on structs"),
}
.into()
}
#[proc_macro_derive(AhkLibrary)]
pub fn ahk_library(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Enum(DataEnum { variants, .. }) => {
let enums = variants.iter().filter(|&v| {
matches!(v.fields, Fields::Unit) || matches!(v.fields, Fields::Unnamed(..))
});
let mut stream = ::proc_macro2::TokenStream::new();
for variant in enums.clone() {
match &variant.fields {
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
for field in unnamed {
stream.extend(quote! {
v.push(#field::generate_ahk_function());
});
}
}
Fields::Unit => {
let name = &variant.ident;
stream.extend(quote! {
v.push(::std::format!(r#"
{}() {{
Run, komorebic.exe {}, , Hide
}}"#,
::std::stringify!(#name),
::std::stringify!(#name).to_kebab_case()
));
});
}
Fields::Named(_) => {
unreachable!("only to be used with unnamed and unit fields");
}
}
}
quote! {
impl #name {
fn generate_ahk_library() -> String {
let mut v: Vec<String> = vec![String::from("; Generated by komorebic.exe")];
#stream
v.join("\n")
}
}
}
}
_ => unreachable!("only to be used on enums"),
}
.into()
}

42
justfile Normal file
View File

@@ -0,0 +1,42 @@
set shell := ["cmd.exe", "/C"]
export RUST_BACKTRACE := "full"
clean:
cargo clean
fmt:
cargo +nightly fmt
cargo +nightly clippy
prettier --write README.md
install-komorebic:
cargo +stable install --path komorebic --locked
install-komorebi:
cargo +stable install --path komorebi --locked
install:
just install-komorebic
just install-komorebi
komorebic ahk-library
cat '%USERPROFILE%\komorebic.lib.ahk' > komorebic.lib.sample.ahk
run:
just install-komorebic
cargo +stable run --bin komorebi --locked
warn $RUST_LOG="warn":
just run
info $RUST_LOG="info":
just run
debug $RUST_LOG="debug":
just run
trace $RUST_LOG="trace":
just run
deadlock $RUST_LOG="trace":
just install-komorebic
cargo +stable run --bin komorebi --locked --features deadlock_detection

View File

@@ -1,15 +1,20 @@
[package]
name = "komorebi-core"
version = "0.1.2"
edition = "2018"
version = "0.1.7"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bindings = { package = "bindings", path = "../bindings" }
clap = "3.0.0-beta.4"
clap = { version = "3", features = ["derive"] }
color-eyre = "0.5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
strum = { version = "0.21", features = ["derive"] }
serde_yaml = "0.8"
strum = { version = "0.23", features = ["derive"] }
[dependencies.windows]
version = "0.30"
features = [
"Win32_Foundation",
]

View File

@@ -0,0 +1,584 @@
use std::num::NonZeroUsize;
use clap::ArgEnum;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Rect;
pub trait Arrangement {
fn calculate(
&self,
area: &Rect,
len: NonZeroUsize,
container_padding: Option<i32>,
layout_flip: Option<Axis>,
resize_dimensions: &[Option<Rect>],
) -> Vec<Rect>;
}
impl Arrangement for DefaultLayout {
#[allow(clippy::too_many_lines)]
fn calculate(
&self,
area: &Rect,
len: NonZeroUsize,
container_padding: Option<i32>,
layout_flip: Option<Axis>,
resize_dimensions: &[Option<Rect>],
) -> Vec<Rect> {
let len = usize::from(len);
let mut dimensions = match self {
DefaultLayout::BSP => recursive_fibonacci(
0,
len,
area,
layout_flip,
calculate_resize_adjustments(resize_dimensions),
),
DefaultLayout::Columns => columns(area, len),
DefaultLayout::Rows => rows(area, len),
DefaultLayout::VerticalStack => {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
1 => area.right,
_ => area.right / 2,
};
let mut main_left = area.left;
let mut stack_left = area.left + primary_right;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
main_left = main_left + area.right - primary_right;
stack_left = area.left;
}
_ => {}
}
if len >= 1 {
layouts.push(Rect {
left: main_left,
top: area.top,
right: primary_right,
bottom: area.bottom,
});
if len > 1 {
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
right: area.right - primary_right,
bottom: area.bottom,
},
len - 1,
));
}
}
layouts
}
DefaultLayout::HorizontalStack => {
let mut layouts: Vec<Rect> = vec![];
let bottom = match len {
1 => area.bottom,
_ => area.bottom / 2,
};
let mut main_top = area.top;
let mut stack_top = area.top + bottom;
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) if len > 1 => {
main_top = main_top + area.bottom - bottom;
stack_top = area.top;
}
_ => {}
}
if len >= 1 {
layouts.push(Rect {
left: area.left,
top: main_top,
right: area.right,
bottom,
});
if len > 1 {
layouts.append(&mut columns(
&Rect {
left: area.left,
top: stack_top,
right: area.right,
bottom: area.bottom - bottom,
},
len - 1,
));
}
}
layouts
}
DefaultLayout::UltrawideVerticalStack => {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
1 => area.right,
_ => area.right / 2,
};
let secondary_right = match len {
1 => 0,
2 => area.right - primary_right,
_ => (area.right - primary_right) / 2,
};
let (primary_left, secondary_left, stack_left) = match len {
1 => (area.left, 0, 0),
2 => {
let mut primary = area.left + secondary_right;
let mut secondary = area.left;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
primary = area.left;
secondary = area.left + primary_right;
}
_ => {}
}
(primary, secondary, 0)
}
_ => {
let primary = area.left + secondary_right;
let mut secondary = area.left;
let mut stack = area.left + primary_right + secondary_right;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
secondary = area.left + primary_right + secondary_right;
stack = area.left;
}
_ => {}
}
(primary, secondary, stack)
}
};
if len >= 1 {
layouts.push(Rect {
left: primary_left,
top: area.top,
right: primary_right,
bottom: area.bottom,
});
if len >= 2 {
layouts.push(Rect {
left: secondary_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
});
if len > 2 {
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
},
len - 2,
));
}
}
}
layouts
}
};
dimensions
.iter_mut()
.for_each(|l| l.add_padding(container_padding));
dimensions
}
}
impl Arrangement for CustomLayout {
fn calculate(
&self,
area: &Rect,
len: NonZeroUsize,
container_padding: Option<i32>,
_layout_flip: Option<Axis>,
_resize_dimensions: &[Option<Rect>],
) -> Vec<Rect> {
let mut dimensions = vec![];
let container_count = len.get();
if container_count <= self.len() {
let mut layouts = columns(area, container_count);
dimensions.append(&mut layouts);
} else {
let count_map = self.column_container_counts();
// If there are not enough windows to trigger the final tertiary
// column in the custom layout, use an offset to reduce the number of
// columns to calculate each column's area by, so that we don't have
// an empty ghost tertiary column and the screen space can be maximised
// until there are enough windows to create it
let mut tertiary_trigger_threshold = 0;
// always -1 because we don't insert the tertiary column in the count_map
for i in 0..self.len() - 1 {
tertiary_trigger_threshold += count_map.get(&i).unwrap();
}
let enable_tertiary_column = len.get() > tertiary_trigger_threshold;
let offset = if enable_tertiary_column {
None
} else {
Option::from(1)
};
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let primary_right = self.primary_width_percentage().map_or_else(
|| area.right / self.len() as i32,
|percentage| (area.right / 100) * percentage as i32,
);
for (idx, column) in self.iter().enumerate() {
// If we are offsetting a tertiary column for which the threshold
// has not yet been met, this loop should not run for that final
// tertiary column
if idx < self.len() - offset.unwrap_or(0) {
let column_area = if idx == 0 {
Self::column_area_with_last(self.len(), area, primary_right, None, offset)
} else {
Self::column_area_with_last(
self.len(),
area,
primary_right,
Option::from(dimensions[self.first_container_idx(idx - 1)]),
offset,
)
};
match column {
Column::Primary(Option::Some(_)) => {
let main_column_area = if idx == 0 {
Self::main_column_area(area, primary_right, None)
} else {
Self::main_column_area(
area,
primary_right,
Option::from(dimensions[self.first_container_idx(idx - 1)]),
)
};
dimensions.push(main_column_area);
}
Column::Primary(None) | Column::Secondary(None) => {
dimensions.push(column_area);
}
Column::Secondary(Some(split)) => match split {
ColumnSplitWithCapacity::Horizontal(capacity) => {
let mut rows = rows(&column_area, *capacity);
dimensions.append(&mut rows);
}
ColumnSplitWithCapacity::Vertical(capacity) => {
let mut columns = columns(&column_area, *capacity);
dimensions.append(&mut columns);
}
},
Column::Tertiary(split) => {
let column_area = Self::column_area_with_last(
self.len(),
area,
primary_right,
Option::from(dimensions[self.first_container_idx(idx - 1)]),
offset,
);
let remaining = container_count - tertiary_trigger_threshold;
match split {
ColumnSplit::Horizontal => {
let mut rows = rows(&column_area, remaining);
dimensions.append(&mut rows);
}
ColumnSplit::Vertical => {
let mut columns = columns(&column_area, remaining);
dimensions.append(&mut columns);
}
}
}
}
}
}
}
dimensions
.iter_mut()
.for_each(|l| l.add_padding(container_padding));
dimensions
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum Axis {
Horizontal,
Vertical,
HorizontalAndVertical,
}
#[must_use]
fn columns(area: &Rect, len: usize) -> Vec<Rect> {
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let right = area.right / len as i32;
let mut left = 0;
let mut layouts: Vec<Rect> = vec![];
for _ in 0..len {
layouts.push(Rect {
left: area.left + left,
top: area.top,
right,
bottom: area.bottom,
});
left += right;
}
layouts
}
#[must_use]
fn rows(area: &Rect, len: usize) -> Vec<Rect> {
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let bottom = area.bottom / len as i32;
let mut top = 0;
let mut layouts: Vec<Rect> = vec![];
for _ in 0..len {
layouts.push(Rect {
left: area.left,
top: area.top + top,
right: area.right,
bottom,
});
top += bottom;
}
layouts
}
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
let mut resize_adjustments = resize_dimensions.to_vec();
// This needs to be aware of layout flips
for (i, opt) in resize_dimensions.iter().enumerate() {
if let Some(resize_ref) = opt {
if i > 0 {
if resize_ref.left != 0 {
#[allow(clippy::if_not_else)]
let range = if i == 1 {
0..1
} else if i & 1 != 0 {
i - 1..i
} else {
i - 2..i
};
for n in range {
let should_adjust = n % 2 == 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.right += resize_ref.left;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: resize_ref.left,
bottom: 0,
});
}
}
}
if let Some(rr) = resize_adjustments[i].as_mut() {
rr.left = 0;
}
}
if resize_ref.top != 0 {
let range = if i == 1 {
0..1
} else if i & 1 == 0 {
i - 1..i
} else {
i - 2..i
};
for n in range {
let should_adjust = n % 2 != 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.bottom += resize_ref.top;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: 0,
bottom: resize_ref.top,
});
}
}
}
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
resize.top = 0;
}
}
}
}
}
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
.iter()
.map(|adjustment| match adjustment {
None => None,
Some(rect) if rect.eq(&Rect::default()) => None,
Some(_) => *adjustment,
})
.collect();
cleaned_resize_adjustments
}
fn recursive_fibonacci(
idx: usize,
count: usize,
area: &Rect,
layout_flip: Option<Axis>,
resize_adjustments: Vec<Option<Rect>>,
) -> Vec<Rect> {
let mut a = *area;
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
a.left += r.left;
a.top += r.top;
a.right += r.right;
a.bottom += r.bottom;
a
} else {
*area
};
let half_width = area.right / 2;
let half_height = area.bottom / 2;
let half_resized_width = resized.right / 2;
let half_resized_height = resized.bottom / 2;
let (main_x, alt_x, alt_y, main_y);
if let Some(flip) = layout_flip {
match flip {
Axis::Horizontal => {
main_x = resized.left + half_width + (half_width - half_resized_width);
alt_x = resized.left;
alt_y = resized.top + half_resized_height;
main_y = resized.top;
}
Axis::Vertical => {
main_y = resized.top + half_height + (half_height - half_resized_height);
alt_y = resized.top;
main_x = resized.left;
alt_x = resized.left + half_resized_width;
}
Axis::HorizontalAndVertical => {
main_x = resized.left + half_width + (half_width - half_resized_width);
alt_x = resized.left;
main_y = resized.top + half_height + (half_height - half_resized_height);
alt_y = resized.top;
}
}
} else {
main_x = resized.left;
alt_x = resized.left + half_resized_width;
main_y = resized.top;
alt_y = resized.top + half_resized_height;
}
#[allow(clippy::if_not_else)]
if count == 0 {
vec![]
} else if count == 1 {
vec![Rect {
left: resized.left,
top: resized.top,
right: resized.right,
bottom: resized.bottom,
}]
} else if idx % 2 != 0 {
let mut res = vec![Rect {
left: resized.left,
top: main_y,
right: resized.right,
bottom: half_resized_height,
}];
res.append(&mut recursive_fibonacci(
idx + 1,
count - 1,
&Rect {
left: area.left,
top: alt_y,
right: area.right,
bottom: area.bottom - half_resized_height,
},
layout_flip,
resize_adjustments,
));
res
} else {
let mut res = vec![Rect {
left: main_x,
top: resized.top,
right: half_resized_width,
bottom: resized.bottom,
}];
res.append(&mut recursive_fibonacci(
idx + 1,
count - 1,
&Rect {
left: alt_x,
top: area.top,
right: area.right - half_resized_width,
bottom: area.bottom,
},
layout_flip,
resize_adjustments,
));
res
}
}

View File

@@ -0,0 +1,277 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::PathBuf;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
use crate::Rect;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CustomLayout(Vec<Column>);
impl Deref for CustomLayout {
type Target = Vec<Column>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CustomLayout {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl CustomLayout {
pub fn from_path_buf(path: PathBuf) -> Result<Self> {
let invalid_filetype = anyhow!("custom layouts must be json or yaml files");
let layout: Self = match path.extension() {
Some(extension) => {
if extension == "yaml" || extension == "yml" {
serde_yaml::from_reader(BufReader::new(File::open(path)?))?
} else if extension == "json" {
serde_json::from_reader(BufReader::new(File::open(path)?))?
} else {
return Err(invalid_filetype);
}
}
None => return Err(invalid_filetype),
};
if !layout.is_valid() {
return Err(anyhow!("the layout file provided was invalid"));
}
Ok(layout)
}
#[must_use]
pub fn column_with_idx(&self, idx: usize) -> (usize, Option<&Column>) {
let column_idx = self.column_for_container_idx(idx);
let column = self.get(column_idx);
(column_idx, column)
}
#[must_use]
pub fn primary_idx(&self) -> Option<usize> {
for (i, column) in self.iter().enumerate() {
if let Column::Primary(_) = column {
return Option::from(i);
}
}
None
}
#[must_use]
pub fn primary_width_percentage(&self) -> Option<usize> {
for column in self.iter() {
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(percentage))) = column
{
return Option::from(*percentage);
}
}
None
}
pub fn set_primary_width_percentage(&mut self, percentage: usize) {
for column in self.iter_mut() {
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(current))) = column {
*current = percentage;
}
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
// A valid layout must have at least one column
if self.is_empty() {
return false;
};
// Vertical column splits aren't supported at the moment
for column in self.iter() {
match column {
Column::Tertiary(ColumnSplit::Vertical)
| Column::Secondary(Some(ColumnSplitWithCapacity::Vertical(_))) => return false,
_ => {}
}
}
// The final column must not have a fixed capacity
match self.last() {
Some(Column::Tertiary(_)) => {}
_ => return false,
}
let mut primaries = 0;
let mut tertiaries = 0;
for column in self.iter() {
match column {
Column::Primary(_) => primaries += 1,
Column::Tertiary(_) => tertiaries += 1,
Column::Secondary(_) => {}
}
}
// There must only be one primary and one tertiary column
matches!(primaries, 1) && matches!(tertiaries, 1)
}
pub(crate) fn column_container_counts(&self) -> HashMap<usize, usize> {
let mut count_map = HashMap::new();
for (idx, column) in self.iter().enumerate() {
match column {
Column::Primary(_) | Column::Secondary(None) => {
count_map.insert(idx, 1);
}
Column::Secondary(Some(split)) => {
count_map.insert(
idx,
match split {
ColumnSplitWithCapacity::Vertical(n)
| ColumnSplitWithCapacity::Horizontal(n) => *n,
},
);
}
Column::Tertiary(_) => {}
}
}
count_map
}
#[must_use]
pub fn first_container_idx(&self, col_idx: usize) -> usize {
let count_map = self.column_container_counts();
let mut container_idx_accumulator = 0;
for i in 0..col_idx {
if let Some(n) = count_map.get(&i) {
container_idx_accumulator += n;
}
}
container_idx_accumulator
}
#[must_use]
pub fn column_for_container_idx(&self, idx: usize) -> usize {
let count_map = self.column_container_counts();
let mut container_idx_accumulator = 0;
// always -1 because we don't insert the tertiary column in the count_map
for i in 0..self.len() - 1 {
if let Some(n) = count_map.get(&i) {
container_idx_accumulator += n;
// The accumulator becomes greater than the window container index
// for the first time when we reach a column that contains that
// window container index
if container_idx_accumulator > idx {
return i;
}
}
}
// If the accumulator never reaches a point where it is greater than the
// window container index, then the only remaining possibility is the
// final tertiary column
self.len() - 1
}
#[must_use]
pub fn column_area(&self, work_area: &Rect, idx: usize, offset: Option<usize>) -> Rect {
let divisor = offset.map_or_else(|| self.len(), |offset| self.len() - offset);
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let equal_width = work_area.right / divisor as i32;
let mut left = work_area.left;
let right = equal_width;
for _ in 0..idx {
left += right;
}
Rect {
left,
top: work_area.top,
right,
bottom: work_area.bottom,
}
}
#[must_use]
pub fn column_area_with_last(
len: usize,
work_area: &Rect,
primary_right: i32,
last_column: Option<Rect>,
offset: Option<usize>,
) -> Rect {
let divisor = offset.map_or_else(|| len - 1, |offset| len - offset - 1);
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let equal_width = (work_area.right - primary_right) / divisor as i32;
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
let right = equal_width;
Rect {
left,
top: work_area.top,
right,
bottom: work_area.bottom,
}
}
#[must_use]
pub fn main_column_area(
work_area: &Rect,
primary_right: i32,
last_column: Option<Rect>,
) -> Rect {
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
Rect {
left,
top: work_area.top,
right: primary_right,
bottom: work_area.bottom,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(tag = "column", content = "configuration")]
pub enum Column {
Primary(Option<ColumnWidth>),
Secondary(Option<ColumnSplitWithCapacity>),
Tertiary(ColumnSplit),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ColumnWidth {
WidthPercentage(usize),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ColumnSplit {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ColumnSplitWithCapacity {
Horizontal(usize),
Vertical(usize),
}

View File

@@ -1,3 +1,5 @@
use std::num::NonZeroUsize;
use clap::ArgEnum;
use serde::Deserialize;
use serde::Serialize;
@@ -13,17 +15,17 @@ pub enum CycleDirection {
impl CycleDirection {
#[must_use]
pub const fn next_idx(&self, idx: usize, len: usize) -> usize {
pub const fn next_idx(&self, idx: usize, len: NonZeroUsize) -> usize {
match self {
CycleDirection::Previous => {
Self::Previous => {
if idx == 0 {
len - 1
len.get() - 1
} else {
idx - 1
}
}
CycleDirection::Next => {
if idx == len - 1 {
Self::Next => {
if idx == len.get() - 1 {
0
} else {
idx + 1

View File

@@ -0,0 +1,125 @@
use clap::ArgEnum;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::OperationDirection;
use crate::Rect;
use crate::Sizing;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum DefaultLayout {
BSP,
Columns,
Rows,
VerticalStack,
HorizontalStack,
UltrawideVerticalStack,
}
impl DefaultLayout {
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn resize(
&self,
unaltered: &Rect,
resize: &Option<Rect>,
edge: OperationDirection,
sizing: Sizing,
delta: i32,
) -> Option<Rect> {
if !matches!(self, Self::BSP) {
return None;
};
let max_divisor = 1.005;
let mut r = resize.unwrap_or_default();
let resize_delta = delta;
match edge {
OperationDirection::Left => match sizing {
Sizing::Increase => {
// Some final checks to make sure the user can't infinitely resize to
// the point of pushing other windows out of bounds
// Note: These checks cannot take into account the changes made to the
// edges of adjacent windows at operation time, so it is still possible
// to push windows out of bounds by maxing out an Increase Left on a
// Window with index 1, and then maxing out a Decrease Right on a Window
// with index 0. I don't think it's worth trying to defensively program
// against this; if people end up in this situation they are better off
// just hitting the retile command
let diff = ((r.left + -resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.left += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.left - -resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.left -= -resize_delta;
}
}
},
OperationDirection::Up => match sizing {
Sizing::Increase => {
let diff = ((r.top + resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.top += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.top - resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.top -= -resize_delta;
}
}
},
OperationDirection::Right => match sizing {
Sizing::Increase => {
let diff = ((r.right + resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.right += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.right - resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.right -= resize_delta;
}
}
},
OperationDirection::Down => match sizing {
Sizing::Increase => {
let diff = ((r.bottom + resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.bottom += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.bottom - resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.bottom -= resize_delta;
}
}
},
};
if r.eq(&Rect::default()) {
None
} else {
Option::from(r)
}
}
}

View File

@@ -0,0 +1,289 @@
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::custom_layout::CustomLayout;
use crate::DefaultLayout;
use crate::OperationDirection;
pub trait Direction {
fn index_in_direction(
&self,
op_direction: OperationDirection,
idx: usize,
count: usize,
) -> Option<usize>;
fn is_valid_direction(
&self,
op_direction: OperationDirection,
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;
}
impl Direction for DefaultLayout {
fn index_in_direction(
&self,
op_direction: OperationDirection,
idx: usize,
count: usize,
) -> Option<usize> {
match op_direction {
OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.left_index(idx))
} else {
None
}
}
OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.right_index(idx))
} else {
None
}
}
OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.up_index(idx))
} else {
None
}
}
OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.down_index(idx))
} else {
None
}
}
}
}
fn is_valid_direction(
&self,
op_direction: OperationDirection,
idx: usize,
count: usize,
) -> bool {
match op_direction {
OperationDirection::Up => match self {
DefaultLayout::BSP => count > 2 && idx != 0 && idx != 1,
DefaultLayout::Columns => false,
DefaultLayout::Rows | DefaultLayout::HorizontalStack => idx != 0,
DefaultLayout::VerticalStack => idx != 0 && idx != 1,
DefaultLayout::UltrawideVerticalStack => idx > 2,
},
OperationDirection::Down => match self {
DefaultLayout::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
DefaultLayout::Columns => false,
DefaultLayout::Rows => idx != count - 1,
DefaultLayout::VerticalStack => idx != 0 && idx != count - 1,
DefaultLayout::HorizontalStack => idx == 0,
DefaultLayout::UltrawideVerticalStack => idx > 1 && idx != count - 1,
},
OperationDirection::Left => match self {
DefaultLayout::BSP => count > 1 && idx != 0,
DefaultLayout::Columns | DefaultLayout::VerticalStack => idx != 0,
DefaultLayout::Rows => false,
DefaultLayout::HorizontalStack => idx != 0 && idx != 1,
DefaultLayout::UltrawideVerticalStack => count > 1 && idx != 1,
},
OperationDirection::Right => match self {
DefaultLayout::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
DefaultLayout::Columns => idx != count - 1,
DefaultLayout::Rows => false,
DefaultLayout::VerticalStack => idx == 0,
DefaultLayout::HorizontalStack => idx != 0 && idx != count - 1,
DefaultLayout::UltrawideVerticalStack => match count {
0 | 1 => false,
2 => idx != 0,
_ => idx < 2,
},
},
}
}
fn up_index(&self, idx: usize) -> usize {
match self {
DefaultLayout::BSP => {
if idx % 2 == 0 {
idx - 1
} else {
idx - 2
}
}
DefaultLayout::Columns => unreachable!(),
DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::UltrawideVerticalStack => idx - 1,
DefaultLayout::HorizontalStack => 0,
}
}
fn down_index(&self, idx: usize) -> usize {
match self {
DefaultLayout::BSP
| DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::UltrawideVerticalStack => idx + 1,
DefaultLayout::Columns => unreachable!(),
DefaultLayout::HorizontalStack => 1,
}
}
fn left_index(&self, idx: usize) -> usize {
match self {
DefaultLayout::BSP => {
if idx % 2 == 0 {
idx - 2
} else {
idx - 1
}
}
DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx - 1,
DefaultLayout::Rows => unreachable!(),
DefaultLayout::VerticalStack => 0,
DefaultLayout::UltrawideVerticalStack => match idx {
0 => 1,
1 => unreachable!(),
_ => 0,
},
}
}
fn right_index(&self, idx: usize) -> usize {
match self {
DefaultLayout::BSP | DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx + 1,
DefaultLayout::Rows => unreachable!(),
DefaultLayout::VerticalStack => 1,
DefaultLayout::UltrawideVerticalStack => match idx {
1 => 0,
0 => 2,
_ => unreachable!(),
},
}
}
}
impl Direction for CustomLayout {
fn index_in_direction(
&self,
op_direction: OperationDirection,
idx: usize,
count: usize,
) -> Option<usize> {
if count <= self.len() {
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
}
match op_direction {
OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.left_index(idx))
} else {
None
}
}
OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.right_index(idx))
} else {
None
}
}
OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.up_index(idx))
} else {
None
}
}
OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.down_index(idx))
} else {
None
}
}
}
}
fn is_valid_direction(
&self,
op_direction: OperationDirection,
idx: usize,
count: usize,
) -> bool {
if count <= self.len() {
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
}
match op_direction {
OperationDirection::Left => idx != 0 && self.column_for_container_idx(idx) != 0,
OperationDirection::Right => {
idx != count - 1 && self.column_for_container_idx(idx) != self.len() - 1
}
OperationDirection::Up => {
if idx == 0 {
return false;
}
let (column_idx, column) = self.column_with_idx(idx);
match column {
None => false,
Some(column) => match column {
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
| Column::Tertiary(ColumnSplit::Horizontal) => {
self.column_for_container_idx(idx - 1) == column_idx
}
_ => false,
},
}
}
OperationDirection::Down => {
if idx == count - 1 {
return false;
}
let (column_idx, column) = self.column_with_idx(idx);
match column {
None => false,
Some(column) => match column {
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
| Column::Tertiary(ColumnSplit::Horizontal) => {
self.column_for_container_idx(idx + 1) == column_idx
}
_ => false,
},
}
}
}
}
fn up_index(&self, idx: usize) -> usize {
idx - 1
}
fn down_index(&self, idx: usize) -> usize {
idx + 1
}
fn left_index(&self, idx: usize) -> usize {
let column_idx = self.column_for_container_idx(idx);
if column_idx - 1 == 0 {
0
} else {
self.first_container_idx(column_idx - 1)
}
}
fn right_index(&self, idx: usize) -> usize {
let column_idx = self.column_for_container_idx(idx);
self.first_container_idx(column_idx + 1)
}
}

View File

@@ -1,388 +1,31 @@
use std::num::NonZeroUsize;
use clap::ArgEnum;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::OperationDirection;
use crate::Rect;
use crate::Sizing;
use crate::Arrangement;
use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Direction;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Layout {
BSP,
Columns,
Rows,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum Flip {
Horizontal,
Vertical,
HorizontalAndVertical,
Default(DefaultLayout),
Custom(CustomLayout),
}
impl Layout {
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn resize(
&self,
unaltered: &Rect,
resize: &Option<Rect>,
edge: OperationDirection,
sizing: Sizing,
step: Option<i32>,
) -> Option<Rect> {
if !matches!(self, Self::BSP) {
return None;
};
let max_divisor = 1.005;
let mut r = resize.unwrap_or_default();
let resize_step = step.unwrap_or(50);
match edge {
OperationDirection::Left => match sizing {
Sizing::Increase => {
// Some final checks to make sure the user can't infinitely resize to
// the point of pushing other windows out of bounds
// Note: These checks cannot take into account the changes made to the
// edges of adjacent windows at operation time, so it is still possible
// to push windows out of bounds by maxing out an Increase Left on a
// Window with index 1, and then maxing out a Decrease Right on a Window
// with index 0. I don't think it's worth trying to defensively program
// against this; if people end up in this situation they are better off
// just hitting the retile command
let diff = ((r.left + -resize_step) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.left += -resize_step;
}
}
Sizing::Decrease => {
let diff = ((r.left - -resize_step) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.left -= -resize_step;
}
}
},
OperationDirection::Up => match sizing {
Sizing::Increase => {
let diff = ((r.top + resize_step) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.top += -resize_step;
}
}
Sizing::Decrease => {
let diff = ((r.top - resize_step) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.top -= -resize_step;
}
}
},
OperationDirection::Right => match sizing {
Sizing::Increase => {
let diff = ((r.right + resize_step) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.right += resize_step;
}
}
Sizing::Decrease => {
let diff = ((r.right - resize_step) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.right -= resize_step;
}
}
},
OperationDirection::Down => match sizing {
Sizing::Increase => {
let diff = ((r.bottom + resize_step) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.bottom += resize_step;
}
}
Sizing::Decrease => {
let diff = ((r.bottom - resize_step) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.bottom -= resize_step;
}
}
},
};
if r.eq(&Rect::default()) {
None
} else {
Option::from(r)
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
match self {
Layout::Default(layout) => Box::new(*layout),
Layout::Custom(layout) => Box::new(layout.clone()),
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
pub fn calculate(
&self,
area: &Rect,
len: NonZeroUsize,
container_padding: Option<i32>,
layout_flip: Option<Flip>,
resize_dimensions: &[Option<Rect>],
) -> Vec<Rect> {
let len = usize::from(len);
let mut dimensions = match self {
Layout::BSP => recursive_fibonacci(
0,
len,
area,
layout_flip,
calculate_resize_adjustments(resize_dimensions),
),
Layout::Columns => {
let right = area.right / len as i32;
let mut left = 0;
let mut layouts: Vec<Rect> = vec![];
for _ in 0..len {
layouts.push(Rect {
left: area.left + left,
top: area.top,
right,
bottom: area.bottom,
});
left += right;
}
layouts
}
Layout::Rows => {
let bottom = area.bottom / len as i32;
let mut top = 0;
let mut layouts: Vec<Rect> = vec![];
for _ in 0..len {
layouts.push(Rect {
left: area.left,
top: area.top + top,
right: area.right,
bottom,
});
top += bottom;
}
layouts
}
};
dimensions
.iter_mut()
.for_each(|l| l.add_padding(container_padding));
dimensions
}
}
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
let mut resize_adjustments = resize_dimensions.to_vec();
// This needs to be aware of layout flips
for (i, opt) in resize_dimensions.iter().enumerate() {
if let Some(resize_ref) = opt {
if i > 0 {
if resize_ref.left != 0 {
#[allow(clippy::if_not_else)]
let range = if i == 1 {
0..1
} else if i & 1 != 0 {
i - 1..i
} else {
i - 2..i
};
for n in range {
let should_adjust = n % 2 == 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.right += resize_ref.left;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: resize_ref.left,
bottom: 0,
});
}
}
}
if let Some(rr) = resize_adjustments[i].as_mut() {
rr.left = 0;
}
}
if resize_ref.top != 0 {
let range = if i == 1 {
0..1
} else if i & 1 == 0 {
i - 1..i
} else {
i - 2..i
};
for n in range {
let should_adjust = n % 2 != 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.bottom += resize_ref.top;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: 0,
bottom: resize_ref.top,
});
}
}
}
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
resize.top = 0;
}
}
}
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
match self {
Layout::Default(layout) => Box::new(*layout),
Layout::Custom(layout) => Box::new(layout.clone()),
}
}
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
.iter()
.map(|adjustment| match adjustment {
None => None,
Some(rect) if rect.eq(&Rect::default()) => None,
Some(_) => *adjustment,
})
.collect();
cleaned_resize_adjustments
}
fn recursive_fibonacci(
idx: usize,
count: usize,
area: &Rect,
layout_flip: Option<Flip>,
resize_adjustments: Vec<Option<Rect>>,
) -> Vec<Rect> {
let mut a = *area;
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
a.left += r.left;
a.top += r.top;
a.right += r.right;
a.bottom += r.bottom;
a
} else {
*area
};
let half_width = area.right / 2;
let half_height = area.bottom / 2;
let half_resized_width = resized.right / 2;
let half_resized_height = resized.bottom / 2;
let (main_x, alt_x, alt_y, main_y);
if let Some(flip) = layout_flip {
match flip {
Flip::Horizontal => {
main_x = resized.left + half_width + (half_width - half_resized_width);
alt_x = resized.left;
alt_y = resized.top + half_resized_height;
main_y = resized.top;
}
Flip::Vertical => {
main_y = resized.top + half_height + (half_height - half_resized_height);
alt_y = resized.top;
main_x = resized.left;
alt_x = resized.left + half_resized_width;
}
Flip::HorizontalAndVertical => {
main_x = resized.left + half_width + (half_width - half_resized_width);
alt_x = resized.left;
main_y = resized.top + half_height + (half_height - half_resized_height);
alt_y = resized.top;
}
}
} else {
main_x = resized.left;
alt_x = resized.left + half_resized_width;
main_y = resized.top;
alt_y = resized.top + half_resized_height;
}
#[allow(clippy::if_not_else)]
if count == 0 {
vec![]
} else if count == 1 {
vec![Rect {
left: resized.left,
top: resized.top,
right: resized.right,
bottom: resized.bottom,
}]
} else if idx % 2 != 0 {
let mut res = vec![Rect {
left: resized.left,
top: main_y,
right: resized.right,
bottom: half_resized_height,
}];
res.append(&mut recursive_fibonacci(
idx + 1,
count - 1,
&Rect {
left: area.left,
top: alt_y,
right: area.right,
bottom: area.bottom - half_resized_height,
},
layout_flip,
resize_adjustments,
));
res
} else {
let mut res = vec![Rect {
left: main_x,
top: resized.top,
right: half_resized_width,
bottom: resized.bottom,
}];
res.append(&mut recursive_fibonacci(
idx + 1,
count - 1,
&Rect {
left: alt_x,
top: area.top,
right: area.right - half_resized_width,
bottom: area.bottom,
},
layout_flip,
resize_adjustments,
));
res
}
}

View File

@@ -1,6 +1,7 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::path::PathBuf;
use std::str::FromStr;
use clap::ArgEnum;
@@ -10,39 +11,57 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
pub use custom_layout::CustomLayout;
pub use cycle_direction::CycleDirection;
pub use layout::Flip;
pub use default_layout::DefaultLayout;
pub use direction::Direction;
pub use layout::Layout;
pub use operation_direction::OperationDirection;
pub use rect::Rect;
pub mod arrangement;
pub mod custom_layout;
pub mod cycle_direction;
pub mod default_layout;
pub mod direction;
pub mod layout;
pub mod operation_direction;
pub mod rect;
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
#[serde(tag = "type", content = "content")]
pub enum SocketMessage {
// Window / Container Commands
FocusWindow(OperationDirection),
MoveWindow(OperationDirection),
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),
ResizeWindow(OperationDirection, Sizing),
ResizeWindowEdge(OperationDirection, Sizing),
ResizeWindowAxis(Axis, Sizing),
UnstackWindow,
CycleStack(CycleDirection),
MoveContainerToMonitorNumber(usize),
MoveContainerToWorkspaceNumber(usize),
SendContainerToMonitorNumber(usize),
SendContainerToWorkspaceNumber(usize),
MoveWorkspaceToMonitorNumber(usize),
Promote,
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
ToggleWindowContainerBehaviour,
WindowHidingBehaviour(HidingBehaviour),
// Current Workspace Commands
ManageFocusedWindow,
UnmanageFocusedWindow,
AdjustContainerPadding(Sizing, i32),
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(Layout),
FlipLayout(Flip),
ChangeLayout(DefaultLayout),
ChangeLayoutCustom(PathBuf),
FlipLayout(Axis),
// Monitor and Workspace Commands
EnsureWorkspaces(usize, usize),
NewWorkspace,
@@ -50,32 +69,46 @@ pub enum SocketMessage {
Stop,
TogglePause,
Retile,
QuickSave,
QuickLoad,
Save(PathBuf),
Load(PathBuf),
CycleFocusMonitor(CycleDirection),
CycleFocusWorkspace(CycleDirection),
FocusMonitorNumber(usize),
FocusWorkspaceNumber(usize),
FocusMonitorWorkspaceNumber(usize, usize),
ContainerPadding(usize, usize, i32),
WorkspacePadding(usize, usize, i32),
WorkspaceTiling(usize, usize, bool),
WorkspaceName(usize, usize, String),
WorkspaceLayout(usize, usize, Layout),
WorkspaceLayout(usize, usize, DefaultLayout),
WorkspaceLayoutCustom(usize, usize, PathBuf),
// Configuration
ReloadConfiguration,
WatchConfiguration(bool),
InvisibleBorders(Rect),
WorkAreaOffset(Rect),
ResizeDelta(i32),
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
FloatRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
IdentifyTrayApplication(ApplicationIdentifier, String),
IdentifyBorderOverflow(ApplicationIdentifier, String),
State,
FocusFollowsMouse(bool),
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
MouseFollowsFocus(bool),
ToggleMouseFollowsFocus,
AddSubscriber(String),
RemoveSubscriber(String),
}
impl SocketMessage {
pub fn as_bytes(&self) -> Result<Vec<u8>> {
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
Ok(serde_json::from_slice(bytes)?)
}
}
impl FromStr for SocketMessage {
@@ -86,6 +119,15 @@ impl FromStr for SocketMessage {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum StateQuery {
FocusedMonitorIndex,
FocusedWorkspaceIndex,
FocusedContainerIndex,
FocusedWindowIndex,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum ApplicationIdentifier {
@@ -94,6 +136,27 @@ pub enum ApplicationIdentifier {
Title,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum FocusFollowsMouseImplementation {
Komorebi,
Windows,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum WindowContainerBehaviour {
Create,
Append,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum HidingBehaviour {
Hide,
Minimize,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum Sizing {

View File

@@ -1,11 +1,13 @@
use std::num::NonZeroUsize;
use clap::ArgEnum;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::Flip;
use crate::Layout;
use crate::direction::Direction;
use crate::Axis;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
@@ -27,92 +29,35 @@ impl OperationDirection {
}
}
fn flip_direction(direction: Self, layout_flip: Option<Flip>) -> Self {
layout_flip.map_or(direction, |flip| match direction {
fn flip(self, layout_flip: Option<Axis>) -> Self {
layout_flip.map_or(self, |flip| match self {
Self::Left => match flip {
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Right,
Flip::Vertical => direction,
Axis::Horizontal | Axis::HorizontalAndVertical => Self::Right,
Axis::Vertical => self,
},
Self::Right => match flip {
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Left,
Flip::Vertical => direction,
Axis::Horizontal | Axis::HorizontalAndVertical => Self::Left,
Axis::Vertical => self,
},
Self::Up => match flip {
Flip::Vertical | Flip::HorizontalAndVertical => Self::Down,
Flip::Horizontal => direction,
Axis::Vertical | Axis::HorizontalAndVertical => Self::Down,
Axis::Horizontal => self,
},
Self::Down => match flip {
Flip::Vertical | Flip::HorizontalAndVertical => Self::Up,
Flip::Horizontal => direction,
Axis::Vertical | Axis::HorizontalAndVertical => Self::Up,
Axis::Horizontal => self,
},
})
}
#[must_use]
pub fn is_valid(
pub fn destination(
self,
layout: Layout,
layout_flip: Option<Flip>,
layout: &dyn Direction,
layout_flip: Option<Axis>,
idx: usize,
len: usize,
) -> bool {
match Self::flip_direction(self, layout_flip) {
OperationDirection::Up => match layout {
Layout::BSP => len > 2 && idx != 0 && idx != 1,
Layout::Columns => false,
Layout::Rows => idx != 0,
},
OperationDirection::Down => match layout {
Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0,
Layout::Columns => false,
Layout::Rows => idx != len - 1,
},
OperationDirection::Left => match layout {
Layout::BSP => len > 1 && idx != 0,
Layout::Columns => idx != 0,
Layout::Rows => false,
},
OperationDirection::Right => match layout {
Layout::BSP => len > 1 && idx % 2 == 0 && idx != len - 1,
Layout::Columns => idx != len - 1,
Layout::Rows => false,
},
}
}
#[must_use]
pub fn new_idx(self, layout: Layout, layout_flip: Option<Flip>, idx: usize) -> usize {
match Self::flip_direction(self, layout_flip) {
Self::Up => match layout {
Layout::BSP => {
if idx % 2 == 0 {
idx - 1
} else {
idx - 2
}
}
Layout::Columns => unreachable!(),
Layout::Rows => idx - 1,
},
Self::Down => match layout {
Layout::BSP | Layout::Rows => idx + 1,
Layout::Columns => unreachable!(),
},
Self::Left => match layout {
Layout::BSP => {
if idx % 2 == 0 {
idx - 2
} else {
idx - 1
}
}
Layout::Columns => idx - 1,
Layout::Rows => unreachable!(),
},
Self::Right => match layout {
Layout::BSP | Layout::Columns => idx + 1,
Layout::Rows => unreachable!(),
},
}
len: NonZeroUsize,
) -> Option<usize> {
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
}
}

View File

@@ -1,8 +1,8 @@
use serde::Deserialize;
use serde::Serialize;
use windows::Win32::Foundation::RECT;
use bindings::Windows::Win32::Foundation::RECT;
#[derive(Debug, Clone, Copy, Serialize, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
pub struct Rect {
pub left: i32,
pub top: i32,
@@ -10,17 +10,6 @@ pub struct Rect {
pub bottom: i32,
}
impl Default for Rect {
fn default() -> Self {
Self {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
}
}
impl From<RECT> for Rect {
fn from(rect: RECT) -> Self {
Self {

View File

@@ -3,6 +3,9 @@
; Enable hot reloading of changes to this file
Run, komorebic.exe watch-configuration enable, , Hide
; Configure the invisible border dimensions
Run, komorebic.exe invisible-borders 7 0 14 7, , Hide
; Enable focus follows mouse
Run, komorebic.exe focus-follows-mouse enable, , Hide
@@ -52,6 +55,9 @@ Run, komorebic.exe manage-rule exe TIM.exe, , Hide
; Identify applications that close to the tray
Run, komorebic.exe identify-tray-application exe Discord.exe, , Hide
; Identify applications that have overflowing borders
Run, komorebic.exe identify-border-overflow exe Discord.exe, , Hide
; Change the focused window, Alt + Vim direction keys
!h::
Run, komorebic.exe focus left, , Hide

View File

@@ -0,0 +1,243 @@
#SingleInstance Force
#Include %A_ScriptDir%\komorebic.lib.ahk
; Default to minimizing windows when switching workspaces
WindowHidingBehaviour("minimize")
; Enable hot reloading of changes to this file
WatchConfiguration("enable")
; Ensure there are 5 workspaces created on monitor 0
EnsureWorkspaces(0, 5)
; Configure the invisible border dimensions
InvisibleBorders(7, 0, 14, 7)
; Configure the 1st workspace
WorkspaceName(0, 0, "bsp")
; Configure the 2nd workspace
WorkspaceName(0, 1, "columns") ; Optionally set the name of the workspace
WorkspacePadding(0, 1, 30) ; Set the padding around the edge of the screen
ContainerPadding(0, 1, 30) ; Set the padding between the containers on the screen
WorkspaceRule("exe", "slack.exe", 0, 1) ; Always show chat apps on this workspace
; Configure the 3rd workspace
WorkspaceName(0, 2, "thicc")
WorkspacePadding(0, 2, 200) ; Set some super thicc padding
; Configure the 4th workspace
WorkspaceName(0, 3, "matrix")
WorkspacePadding(0, 3, 0) ; No padding at all
ContainerPadding(0, 3, 0) ; Matrix-y hacker vibes
; Configure the 5th workspace
WorkspaceName(0, 4, "floaty")
WorkspaceTiling(0, 4, "disable") ; Everything floats here
; Configure floating rules
FloatRule("class", "SunAwtDialog") ; All the IntelliJ popups
FloatRule("title", "Control Panel")
FloatRule("class", "TaskManagerWindow")
FloatRule("exe", "Wally.exe")
FloatRule("exe", "wincompose.exe")
FloatRule("exe", "1Password.exe")
FloatRule("exe", "Wox.exe")
FloatRule("exe", "ddm.exe")
FloatRule("class", "Chrome_RenderWidgetHostHWND") ; GOG Electron invisible overlay
FloatRule("class", "CEFCLIENT")
; Identify Minimize-to-Tray Applications
IdentifyTrayApplication("exe", "Discord.exe")
IdentifyTrayApplication("exe", "Spotify.exe")
IdentifyTrayApplication("exe", "GalaxyClient.exe")
; Identify Electron applications with overflowing borders
IdentifyBorderOverflow("exe", "Discord.exe")
IdentifyBorderOverflow("exe", "Spotify.exe")
IdentifyBorderOverflow("exe", "GalaxyClient.exe")
IdentifyBorderOverflow("class", "ZPFTEWndClass")
; Identify applications to be forcibly managed
ManageRule("exe", "GalaxyClient.exe")
; Change the focused window, Alt + Vim direction keys
!h::
Focus("left")
return
!j::
Focus("down")
return
!k::
Focus("up")
return
!l::
Focus("right")
return
; Move the focused window in a given direction, Alt + Shift + Vim direction keys
!+h::
Move("left")
return
!+j::
Move("down")
return
!+k::
Move("up")
return
!+l::
Move("right")
return
; Stack the focused window in a given direction, Alt + Shift + direction keys
!+Left::
Stack("left")
return
!+Down::
Stack("down")
return
!+Up::
Stack("up")
return
!+Right::
Stack("right")
return
!]::
CycleStack("next")
return
![::
CycleStack("previous")
return
; Unstack the focused window, Alt + Shift + D
!+d::
Unstack()
return
; Promote the focused window to the top of the tree, Alt + Shift + Enter
!+Enter::
Promote()
return
; Manage the focused window
!=::
Manage()
return
; Unmanage the focused window
!-::
Unmanage()
return
; Switch to an equal-width, max-height column layout on the main workspace, Alt + Shift + C
!+c::
ChangeLayout("columns")
return
; Switch to the default bsp tiling layout on the main workspace, Alt + Shift + T
!+t::
ChangeLayout("bsp")
return
; Toggle the Monocle layout for the focused window, Alt + Shift + F
!+f::
ToggleMonocle()
return
; Toggle native maximize for the focused window, Alt + Shift + =
!+=::
ToggleMaximize()
return
; Flip horizontally, Alt + X
!x::
FlipLayout("horizontal")
return
; Flip vertically, Alt + Y
!y::
FlipLayout("vertical")
return
; Force a retile if things get janky, Alt + Shift + R
!+r::
Retile()
return
; Float the focused window, Alt + T
!t::
ToggleFloat()
return
; Reload ~/komorebi.ahk, Alt + O
!o::
ReloadConfiguration()
return
; Pause responding to any window events or komorebic commands, Alt + P
!p::
TogglePause()
return
; Enable focus follows mouse
!0::
ToggleFocusFollowsMouse("komorebi")
return
; Switch to workspace
!1::
Send !
FocusWorkspace(0)
return
!2::
Send !
FocusWorkspace(1)
return
!3::
Send !
FocusWorkspace(2)
return
!4::
Send !
FocusWorkspace(3)
return
!5::
Send !
FocusWorkspace(4)
return
; Move window to workspace
!+1::
MoveToWorkspace(0)
return
!+2::
MoveToWorkspace(1)
return
!+3::
MoveToWorkspace(2)
return
!+4::
MoveToWorkspace(3)
return
!+5::
MoveToWorkspace(4)
return

View File

@@ -1,21 +1,25 @@
[package]
name = "komorebi"
version = "0.1.2"
edition = "2018"
version = "0.1.7"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
license = "MIT"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bindings = { package = "bindings", path = "../bindings" }
komorebi-core = { path = "../komorebi-core" }
bitflags = "1"
clap = { version = "3", features = ["derive"] }
color-eyre = "0.5"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
ctrlc = "3"
dirs = "3"
eyre = "0.6"
dirs = "4"
getset = "0.1"
hotwatch = "0.4"
lazy_static = "1"
@@ -24,14 +28,29 @@ parking_lot = { version = "0.11", features = ["deadlock_detection"] }
paste = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
strum = { version = "0.21", features = ["derive"] }
sysinfo = "0.20"
strum = { version = "0.23", features = ["derive"] }
sysinfo = "0.22"
tracing = "0.1"
tracing-appender = "0.1"
tracing-subscriber = "0.2"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uds_windows = "1"
which = "4"
winvd = "0.0.20"
winput = "0.2"
miow = "0.4"
winreg = "0.10"
[dependencies.windows]
version = "0.30"
features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_System_Threading",
"Win32_System_RemoteDesktop",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Accessibility",
"Win32_UI_WindowsAndMessaging"
]
[features]
deadlock_detection = []

View File

@@ -44,6 +44,18 @@ impl Container {
}
}
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
for window in self.windows() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(window.hwnd);
}
}
}
None
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for window in self.windows() {
if window.hwnd == hwnd {
@@ -66,18 +78,18 @@ impl Container {
}
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
self.windows_mut().remove(idx)
let window = self.windows_mut().remove(idx);
if idx != 0 {
self.focus_window(idx - 1);
};
window
}
pub fn remove_focused_window(&mut self) -> Option<Window> {
let focused_idx = self.focused_window_idx();
let window = self.remove_window_by_idx(focused_idx);
if focused_idx != 0 {
self.focus_window(focused_idx - 1);
}
window
self.remove_window_by_idx(focused_idx)
}
pub fn add_window(&mut self, window: Window) {

View File

@@ -2,14 +2,20 @@
#![allow(clippy::missing_errors_doc)]
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
#[cfg(feature = "deadlock_detection")]
use std::thread;
#[cfg(feature = "deadlock_detection")]
use std::time::Duration;
use color_eyre::eyre::ContextCompat;
use clap::Parser;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
@@ -17,14 +23,22 @@ use lazy_static::lazy_static;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
use serde::Serialize;
use sysinfo::SystemExt;
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 komorebi_core::HidingBehaviour;
use komorebi_core::SocketMessage;
use crate::process_command::listen_for_commands;
use crate::process_event::listen_for_events;
use crate::process_movement::listen_for_movements;
use crate::window_manager::State;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
@@ -36,6 +50,7 @@ mod container;
mod monitor;
mod process_command;
mod process_event;
mod process_movement;
mod set_window_position;
mod styles;
mod window;
@@ -51,16 +66,15 @@ lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_EXE_WHITELIST: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
static ref TRAY_AND_MULTI_WINDOW_CLASSES: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![]));
static ref TRAY_AND_MULTI_WINDOW_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"explorer.exe".to_string(),
"firefox.exe".to_string(),
"chrome.exe".to_string(),
"idea64.exe".to_string(),
"ApplicationFrameHost.exe".to_string(),
"steam.exe".to_string(),
]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![
"explorer.exe".to_string(),
"firefox.exe".to_string(),
"chrome.exe".to_string(),
"idea64.exe".to_string(),
"ApplicationFrameHost.exe".to_string(),
"steam.exe".to_string(),
]));
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"firefox.exe".to_string(),
"idea64.exe".to_string(),
@@ -68,9 +82,27 @@ lazy_static! {
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, (usize, usize)>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
// mstsc.exe creates these on Windows 11 when a WSL process is launched
// https://github.com/LGUG2Z/komorebi/issues/74
"OPContainerClass".to_string(),
"IHWindowClass".to_string()
]));
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"X410.exe".to_string(),
"mstsc.exe".to_string(),
"vcxsrv.exe".to_string(),
]));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
Arc::new(Mutex::new(HidingBehaviour::Minimize));
}
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
@@ -82,7 +114,7 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
std::env::set_var("RUST_LOG", "info");
}
let home = dirs::home_dir().context("there is no home directory")?;
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let appender = tracing_appender::rolling::never(home, "komorebi.log");
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
@@ -113,27 +145,30 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
// occurred to be recorded.
std::panic::set_hook(Box::new(|panic| {
// If the panic has a source location, record it as structured fields.
if let Some(location) = panic.location() {
// On nightly Rust, where the `PanicInfo` type also exposes a
// `message()` method returning just the message, we could record
// just the message instead of the entire `fmt::Display`
// implementation, avoiding the duplciated location
tracing::error!(
message = %panic,
panic.file = location.file(),
panic.line = location.line(),
panic.column = location.column(),
);
} else {
tracing::error!(message = %panic);
}
panic.location().map_or_else(
|| {
tracing::error!(message = %panic);
},
|location| {
// On nightly Rust, where the `PanicInfo` type also exposes a
// `message()` method returning just the message, we could record
// just the message instead of the entire `fmt::Display`
// implementation, avoiding the duplciated location
tracing::error!(
message = %panic,
panic.file = location.file(),
panic.line = location.line(),
panic.column = location.column(),
);
},
);
}));
Ok((guard, color_guard))
}
pub fn load_configuration() -> Result<()> {
let home = dirs::home_dir().context("there is no home directory")?;
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut config_v1 = home.clone();
config_v1.push("komorebi.ahk");
@@ -147,7 +182,7 @@ pub fn load_configuration() -> Result<()> {
config_v1
.as_os_str()
.to_str()
.context("cannot convert path to string")?
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("autohotkey.exe")
@@ -159,7 +194,7 @@ pub fn load_configuration() -> Result<()> {
config_v2
.as_os_str()
.to_str()
.context("cannot convert path to string")?
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("AutoHotkey64.exe")
@@ -170,6 +205,96 @@ pub fn load_configuration() -> Result<()> {
Ok(())
}
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)]
#[serde(untagged)]
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
}
#[derive(Debug, Serialize)]
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 subscriptions.iter_mut() {
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() {
@@ -193,61 +318,84 @@ fn detect_deadlocks() {
});
}
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(long = "ffm")]
focus_follows_mouse: bool,
}
#[tracing::instrument]
fn main() -> Result<()> {
match std::env::args().count() {
1 => {
let mut system = sysinfo::System::new_all();
system.refresh_processes();
let opts: Opts = Opts::parse();
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
if system.process_by_name("komorebi.exe").len() > 1 {
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
std::process::exit(1);
}
let arg_count = std::env::args().count();
let has_valid_args = arg_count == 1 || (arg_count == 2 && CUSTOM_FFM.load(Ordering::SeqCst));
// File logging worker guard has to have an assignment in the main fn to work
let (_guard, _color_guard) = setup()?;
if has_valid_args {
let session_id = WindowsApi::process_id_to_session_id()?;
SESSION_ID.store(session_id, Ordering::SeqCst);
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
let mut system = sysinfo::System::new_all();
system.refresh_processes();
let process_id = WindowsApi::current_process_id();
WindowsApi::allow_set_foreground_window(process_id)?;
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
incoming,
)))?));
wm.lock().init()?;
listen_for_commands(wm.clone());
listen_for_events(wm.clone());
load_configuration()?;
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
ctrlc_sender
.send(())
.expect("could not send signal on ctrl-c channel");
})?;
ctrlc_receiver
.recv()
.expect("could not receive signal on ctrl-c channel");
tracing::error!(
"received ctrl-c, restoring all hidden windows and terminating process"
);
wm.lock().restore_all_windows();
std::process::exit(130);
if system.process_by_name("komorebi.exe").len() > 1 {
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
std::process::exit(1);
}
_ => Ok(()),
// File logging worker guard has to have an assignment in the main fn to work
let (_guard, _color_guard) = setup()?;
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
let process_id = WindowsApi::current_process_id();
WindowsApi::allow_set_foreground_window(process_id)?;
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
incoming,
)))?));
wm.lock().init()?;
listen_for_commands(wm.clone());
listen_for_events(wm.clone());
if CUSTOM_FFM.load(Ordering::SeqCst) {
listen_for_movements(wm.clone());
}
load_configuration()?;
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
ctrlc_sender
.send(())
.expect("could not send signal on ctrl-c channel");
})?;
ctrlc_receiver
.recv()
.expect("could not receive signal on ctrl-c channel");
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
wm.lock().restore_all_windows();
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
}
std::process::exit(130);
}
Ok(())
}

View File

@@ -1,11 +1,12 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use getset::CopyGetters;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use serde::Serialize;
use komorebi_core::Rect;
@@ -14,12 +15,13 @@ use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters)]
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
pub struct Monitor {
#[getset(get_copy = "pub")]
#[getset(get_copy = "pub", set = "pub")]
id: isize,
monitor_size: Rect,
#[getset(get = "pub")]
#[getset(get = "pub", set = "pub")]
size: Rect,
#[getset(get = "pub", set = "pub")]
work_area_size: Rect,
workspaces: Ring<Workspace>,
#[serde(skip_serializing)]
@@ -29,22 +31,25 @@ pub struct Monitor {
impl_ring_elements!(Monitor, Workspace);
pub fn new(id: isize, monitor_size: Rect, work_area_size: Rect) -> Monitor {
pub fn new(id: isize, size: Rect, work_area_size: Rect) -> Monitor {
let mut workspaces = Ring::default();
workspaces.elements_mut().push_back(Workspace::default());
Monitor {
id,
monitor_size,
size,
work_area_size,
workspaces: Ring::default(),
workspaces,
workspace_names: HashMap::default(),
}
}
impl Monitor {
pub fn load_focused_workspace(&mut self) -> Result<()> {
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
let focused_idx = self.focused_workspace_idx();
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
if i == focused_idx {
workspace.restore()?;
workspace.restore(mouse_follows_focus)?;
} else {
workspace.hide();
}
@@ -56,13 +61,27 @@ impl Monitor {
pub fn add_container(&mut self, container: Container) -> Result<()> {
let workspace = self
.focused_workspace_mut()
.context("there is no workspace")?;
.ok_or_else(|| anyhow!("there is no workspace"))?;
workspace.add_container(container);
Ok(())
}
pub fn remove_workspace_by_idx(&mut self, idx: usize) -> Option<Workspace> {
if idx < self.workspaces().len() {
return self.workspaces_mut().remove(idx);
}
if idx == 0 {
self.workspaces_mut().push_back(Workspace::default());
} else {
self.focus_workspace(idx - 1).ok()?;
};
None
}
pub fn ensure_workspace_count(&mut self, ensure_count: usize) {
if self.workspaces().len() < ensure_count {
self.workspaces_mut()
@@ -78,17 +97,17 @@ impl Monitor {
) -> Result<()> {
let workspace = self
.focused_workspace_mut()
.context("there is no workspace")?;
.ok_or_else(|| anyhow!("there is no workspace"))?;
if workspace.maximized_window().is_some() {
return Err(eyre::anyhow!(
return Err(anyhow!(
"cannot move native maximized window to another monitor or workspace"
));
}
let container = workspace
.remove_focused_container()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let workspaces = self.workspaces_mut();
@@ -129,7 +148,7 @@ impl Monitor {
if name.is_some() {
self.workspaces_mut()
.get_mut(idx)
.context("there is no workspace")?
.ok_or_else(|| anyhow!("there is no workspace"))?
.set_name(name);
}
}
@@ -141,12 +160,16 @@ impl Monitor {
self.workspaces().len()
}
pub fn update_focused_workspace(&mut self) -> Result<()> {
pub fn update_focused_workspace(
&mut self,
offset: Option<Rect>,
invisible_borders: &Rect,
) -> Result<()> {
let work_area = *self.work_area_size();
self.focused_workspace_mut()
.context("there is no workspace")?
.update(&work_area)?;
.ok_or_else(|| anyhow!("there is no workspace"))?
.update(&work_area, offset, invisible_borders)?;
Ok(())
}

View File

@@ -1,25 +1,45 @@
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::num::NonZeroUsize;
use std::str::FromStr;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::thread;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use miow::pipe::connect;
use parking_lot::Mutex;
use uds_windows::UnixStream;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;
use komorebi_core::WindowContainerBehaviour;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::Notification;
use crate::NotificationEvent;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::CUSTOM_FFM;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::MANAGE_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
use crate::TRAY_AND_MULTI_WINDOW_EXES;
use crate::SUBSCRIPTION_PIPES;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
#[tracing::instrument]
@@ -50,7 +70,17 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn process_command(&mut self, message: SocketMessage) -> Result<()> {
self.validate_virtual_desktop_id();
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
if let Some(id) = current_virtual_desktop() {
if id != *virtual_desktop_id {
tracing::info!(
"ignoring events and commands while not on virtual desktop {:?}",
virtual_desktop_id
);
return Ok(());
}
}
}
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
@@ -60,6 +90,12 @@ impl WindowManager {
SocketMessage::MoveWindow(direction) => {
self.move_container_in_direction(direction)?;
}
SocketMessage::CycleFocusWindow(direction) => {
self.focus_container_in_cycle_direction(direction)?;
}
SocketMessage::CycleMoveWindow(direction) => {
self.move_container_in_cycle_direction(direction)?;
}
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
SocketMessage::CycleStack(direction) => {
@@ -88,10 +124,57 @@ impl WindowManager {
manage_identifiers.push(id);
}
}
SocketMessage::FloatRule(_, id) => {
SocketMessage::FloatRule(identifier, id) => {
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
if !float_identifiers.contains(&id) {
float_identifiers.push(id);
float_identifiers.push(id.clone());
}
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let mut hwnds_to_purge = vec![];
for (i, monitor) in self.monitors().iter().enumerate() {
for container in monitor
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace"))?
.containers()
.iter()
{
for window in container.windows().iter() {
match identifier {
ApplicationIdentifier::Exe => {
if window.exe()? == id {
hwnds_to_purge.push((i, window.hwnd));
}
}
ApplicationIdentifier::Class => {
if window.class()? == id {
hwnds_to_purge.push((i, window.hwnd));
}
}
ApplicationIdentifier::Title => {
if window.title()? == id {
hwnds_to_purge.push((i, window.hwnd));
}
}
}
}
}
}
for (monitor_idx, hwnd) in hwnds_to_purge {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no focused workspace"))?
.remove_window(hwnd)?;
monitor.update_focused_workspace(offset, &invisible_borders)?;
}
}
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
@@ -106,41 +189,93 @@ impl WindowManager {
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, true)?;
}
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, false)?;
}
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, false)?;
}
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
self.move_workspace_to_monitor(monitor_idx)?;
}
SocketMessage::TogglePause => {
tracing::info!("pausing");
if self.is_paused {
tracing::info!("resuming");
} else {
tracing::info!("pausing");
}
self.is_paused = !self.is_paused;
self.retile_all(true)?;
}
SocketMessage::ToggleTiling => {
self.toggle_tiling()?;
}
SocketMessage::CycleFocusMonitor(direction) => {
let monitor_idx = direction.next_idx(
self.focused_monitor_idx(),
NonZeroUsize::new(self.monitors().len())
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus)?;
}
SocketMessage::FocusMonitorNumber(monitor_idx) => {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(true)?;
}
SocketMessage::Retile => {
for monitor in self.monitors_mut() {
let work_area = *monitor.work_area_size();
let workspace = monitor
.focused_workspace_mut()
.context("there is no workspace")?;
// Reset any resize adjustments if we want to force a retile
for resize in workspace.resize_dimensions_mut() {
*resize = None;
}
workspace.update(&work_area)?;
}
self.update_focused_workspace(self.mouse_follows_focus)?;
}
SocketMessage::Retile => self.retile_all(false)?,
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::ChangeLayoutCustom(path) => self.change_workspace_custom_layout(path)?,
SocketMessage::WorkspaceLayoutCustom(monitor_idx, workspace_idx, path) => {
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?;
}
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
}
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
self.set_workspace_layout(monitor_idx, workspace_idx, layout)?;
self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?;
}
SocketMessage::CycleFocusWorkspace(direction) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
let monitor_idx = self.monitor_idx_from_current_pos().ok_or_else(|| {
anyhow!("there is no monitor associated with the current cursor position")
})?;
self.focus_monitor(monitor_idx)?;
let focused_monitor = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
let workspaces = focused_monitor.workspaces().len();
let workspace_idx = direction.next_idx(
focused_workspace_idx,
NonZeroUsize::new(workspaces)
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
);
self.focus_workspace(workspace_idx)?;
}
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
let monitor_idx = self.monitor_idx_from_current_pos().ok_or_else(|| {
anyhow!("there is no monitor associated with the current cursor position")
})?;
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
SocketMessage::Stop => {
@@ -148,6 +283,11 @@ impl WindowManager {
"received stop command, restoring all hidden windows and terminating process"
);
self.restore_all_windows();
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
}
std::process::exit(0)
}
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
@@ -160,22 +300,215 @@ impl WindowManager {
self.set_workspace_name(monitor_idx, workspace_idx, name)?;
}
SocketMessage::State => {
let state = serde_json::to_string_pretty(&window_manager::State::from(self))?;
let mut socket = dirs::home_dir().context("there is no home directory")?;
let state = match serde_json::to_string_pretty(&window_manager::State::from(&*self))
{
Ok(state) => state,
Err(error) => error.to_string(),
};
let mut socket =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(&socket)?;
stream.write_all(state.as_bytes())?;
}
SocketMessage::ResizeWindow(direction, sizing) => {
self.resize_window(direction, sizing, Option::from(50))?;
SocketMessage::Query(query) => {
let response = match query {
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
StateQuery::FocusedWorkspaceIndex => self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx(),
StateQuery::FocusedContainerIndex => {
self.focused_workspace()?.focused_container_idx()
}
StateQuery::FocusedWindowIndex => {
self.focused_container()?.focused_window_idx()
}
}
.to_string();
let mut socket =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(&socket)?;
stream.write_all(response.as_bytes())?;
}
SocketMessage::FocusFollowsMouse(enable) => {
if enable {
WindowsApi::enable_focus_follows_mouse()?;
SocketMessage::ResizeWindowEdge(direction, sizing) => {
self.resize_window(direction, sizing, self.resize_delta, true)?;
}
SocketMessage::ResizeWindowAxis(axis, sizing) => {
// If the user has a custom layout, allow for the resizing of the primary column
// with this signal
if let Layout::Custom(ref mut custom) = self.focused_workspace_mut()?.layout_mut() {
if matches!(axis, Axis::Horizontal) {
let percentage = custom
.primary_width_percentage()
.unwrap_or(100 / custom.len());
match sizing {
Sizing::Increase => custom.set_primary_width_percentage(percentage + 5),
Sizing::Decrease => custom.set_primary_width_percentage(percentage - 5),
}
}
// Otherwise proceed with the resizing logic for individual window containers in the
// assumed BSP layout
} else {
WindowsApi::disable_focus_follows_mouse()?;
match axis {
Axis::Horizontal => {
self.resize_window(
OperationDirection::Left,
sizing,
self.resize_delta,
false,
)?;
self.resize_window(
OperationDirection::Right,
sizing,
self.resize_delta,
false,
)?;
}
Axis::Vertical => {
self.resize_window(
OperationDirection::Up,
sizing,
self.resize_delta,
false,
)?;
self.resize_window(
OperationDirection::Down,
sizing,
self.resize_delta,
false,
)?;
}
Axis::HorizontalAndVertical => {
self.resize_window(
OperationDirection::Left,
sizing,
self.resize_delta,
false,
)?;
self.resize_window(
OperationDirection::Right,
sizing,
self.resize_delta,
false,
)?;
self.resize_window(
OperationDirection::Up,
sizing,
self.resize_delta,
false,
)?;
self.resize_window(
OperationDirection::Down,
sizing,
self.resize_delta,
false,
)?;
}
}
}
self.update_focused_workspace(false)?;
}
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
if !CUSTOM_FFM.load(Ordering::SeqCst) {
tracing::warn!(
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be enabled; defaulting to windows implementation"
);
implementation = FocusFollowsMouseImplementation::Windows;
}
match implementation {
FocusFollowsMouseImplementation::Komorebi => {
if WindowsApi::focus_follows_mouse()? {
tracing::warn!(
"the komorebi implementation of focus follows mouse cannot be enabled while the windows implementation is enabled"
);
} else if enable {
self.focus_follows_mouse = Option::from(implementation);
} else {
self.focus_follows_mouse = None;
self.has_pending_raise_op = false;
}
}
FocusFollowsMouseImplementation::Windows => {
if let Some(FocusFollowsMouseImplementation::Komorebi) =
self.focus_follows_mouse
{
tracing::warn!(
"the windows implementation of focus follows mouse cannot be enabled while the komorebi implementation is enabled"
);
} else if enable {
WindowsApi::enable_focus_follows_mouse()?;
self.focus_follows_mouse =
Option::from(FocusFollowsMouseImplementation::Windows);
} else {
WindowsApi::disable_focus_follows_mouse()?;
self.focus_follows_mouse = None;
}
}
}
}
SocketMessage::ToggleFocusFollowsMouse(mut implementation) => {
if !CUSTOM_FFM.load(Ordering::SeqCst) {
tracing::warn!(
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be toggled; defaulting to windows implementation"
);
implementation = FocusFollowsMouseImplementation::Windows;
}
match implementation {
FocusFollowsMouseImplementation::Komorebi => {
if WindowsApi::focus_follows_mouse()? {
tracing::warn!(
"the komorebi implementation of focus follows mouse cannot be toggled while the windows implementation is enabled"
);
} else {
match self.focus_follows_mouse {
None => {
self.focus_follows_mouse = Option::from(implementation);
self.has_pending_raise_op = false;
}
Some(FocusFollowsMouseImplementation::Komorebi) => {
self.focus_follows_mouse = None;
}
Some(FocusFollowsMouseImplementation::Windows) => {
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
}
}
}
}
FocusFollowsMouseImplementation::Windows => {
if let Some(FocusFollowsMouseImplementation::Komorebi) =
self.focus_follows_mouse
{
tracing::warn!(
"the windows implementation of focus follows mouse cannot be toggled while the komorebi implementation is enabled"
);
} else {
match self.focus_follows_mouse {
None => {
WindowsApi::enable_focus_follows_mouse()?;
self.focus_follows_mouse = Option::from(implementation);
}
Some(FocusFollowsMouseImplementation::Windows) => {
WindowsApi::disable_focus_follows_mouse()?;
self.focus_follows_mouse = None;
}
Some(FocusFollowsMouseImplementation::Komorebi) => {
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
}
}
}
}
}
}
SocketMessage::ReloadConfiguration => {
@@ -184,28 +517,125 @@ impl WindowManager {
SocketMessage::WatchConfiguration(enable) => {
self.watch_configuration(enable)?;
}
SocketMessage::IdentifyTrayApplication(identifier, id) => match identifier {
ApplicationIdentifier::Exe => {
let mut exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
if !exes.contains(&id) {
exes.push(id);
}
SocketMessage::IdentifyBorderOverflow(_, id) => {
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
ApplicationIdentifier::Class => {
let mut classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
if !classes.contains(&id) {
classes.push(id);
}
}
SocketMessage::IdentifyTrayApplication(_, id) => {
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
ApplicationIdentifier::Title => {}
},
}
SocketMessage::ManageFocusedWindow => {
self.manage_focused_window()?;
}
SocketMessage::UnmanageFocusedWindow => {
self.unmanage_focused_window()?;
}
}
SocketMessage::InvisibleBorders(rect) => {
self.invisible_borders = rect;
self.retile_all(false)?;
}
SocketMessage::WorkAreaOffset(rect) => {
self.work_area_offset = Option::from(rect);
self.retile_all(false)?;
}
SocketMessage::QuickSave => {
let workspace = self.focused_workspace()?;
let resize = workspace.resize_dimensions();
let mut quicksave_json = std::env::temp_dir();
quicksave_json.push("komorebi.quicksave.json");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(quicksave_json)?;
serde_json::to_writer_pretty(&file, &resize)?;
}
SocketMessage::QuickLoad => {
let workspace = self.focused_workspace_mut()?;
let mut quicksave_json = std::env::temp_dir();
quicksave_json.push("komorebi.quicksave.json");
let file = File::open(&quicksave_json).map_err(|_| {
anyhow!(
"no quicksave found at {}",
quicksave_json.display().to_string()
)
})?;
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
}
SocketMessage::Save(path) => {
let workspace = self.focused_workspace_mut()?;
let resize = workspace.resize_dimensions();
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)?;
serde_json::to_writer_pretty(&file, &resize)?;
}
SocketMessage::Load(path) => {
let workspace = self.focused_workspace_mut()?;
let file = File::open(&path)
.map_err(|_| anyhow!("no file found at {}", path.display().to_string()))?;
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
}
SocketMessage::AddSubscriber(subscriber) => {
let mut pipes = SUBSCRIPTION_PIPES.lock();
let pipe_path = format!(r"\\.\pipe\{}", subscriber);
let pipe = connect(&pipe_path).map_err(|_| {
anyhow!("the named pipe '{}' has not yet been created; please create it before running this command", pipe_path)
})?;
pipes.insert(subscriber, pipe);
}
SocketMessage::RemoveSubscriber(subscriber) => {
let mut pipes = SUBSCRIPTION_PIPES.lock();
pipes.remove(&subscriber);
}
SocketMessage::MouseFollowsFocus(enable) => {
self.mouse_follows_focus = enable;
}
SocketMessage::ToggleMouseFollowsFocus => {
self.mouse_follows_focus = !self.mouse_follows_focus;
}
SocketMessage::ResizeDelta(delta) => {
self.resize_delta = delta;
}
SocketMessage::ToggleWindowContainerBehaviour => {
match self.window_container_behaviour {
WindowContainerBehaviour::Create => {
self.window_container_behaviour = WindowContainerBehaviour::Append;
}
WindowContainerBehaviour::Append => {
self.window_container_behaviour = WindowContainerBehaviour::Create;
}
}
}
SocketMessage::WindowHidingBehaviour(behaviour) => {
let mut hiding_behaviour = HIDING_BEHAVIOUR.lock();
*hiding_behaviour = behaviour;
}
};
tracing::info!("processed");
Ok(())
@@ -218,17 +648,22 @@ impl WindowManager {
let message = SocketMessage::from_str(&line?)?;
if self.is_paused {
if let SocketMessage::TogglePause = message {
tracing::info!("resuming");
self.is_paused = !self.is_paused;
return Ok(());
}
tracing::trace!("ignoring while paused");
return Ok(());
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(self.process_command(message)?)
}
_ => {
tracing::trace!("ignoring while paused");
Ok(())
}
};
}
self.process_command(message)?;
self.process_command(message.clone())?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: (&*self).into(),
})?)?;
}
Ok(())

View File

@@ -2,7 +2,7 @@ use std::fs::OpenOptions;
use std::sync::Arc;
use std::thread;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::select;
use parking_lot::Mutex;
@@ -10,13 +10,17 @@ use parking_lot::Mutex;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::WindowContainerBehaviour;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::Notification;
use crate::NotificationEvent;
use crate::HIDDEN_HWNDS;
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
use crate::TRAY_AND_MULTI_WINDOW_EXES;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
@@ -48,28 +52,43 @@ impl WindowManager {
return Ok(());
}
self.validate_virtual_desktop_id();
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
if let Some(id) = current_virtual_desktop() {
if id != *virtual_desktop_id {
tracing::info!(
"ignoring events and commands while not on virtual desktop {:?}",
virtual_desktop_id
);
return Ok(());
}
}
}
// Make sure we have the most recently focused monitor from any event
match event {
WindowManagerEvent::FocusChange(_, window)
WindowManagerEvent::MonitorPoll(_, window)
| WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::MoveResizeEnd(_, window) => {
let monitor_idx = self
.monitor_idx_from_window(*window)
.context("there is no monitor associated with this window, it may have already been destroyed")?;
self.reconcile_monitors()?;
let monitor_idx = self.monitor_idx_from_window(*window)
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
self.focus_monitor(monitor_idx)?;
}
_ => {}
}
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
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)?;
workspace.update(&work_area, offset, &invisible_borders)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
@@ -91,13 +110,29 @@ impl WindowManager {
}
match event {
WindowManagerEvent::Minimize(_, window)
| WindowManagerEvent::Destroy(_, window)
| WindowManagerEvent::Unmanage(window) => {
WindowManagerEvent::Raise(window) => {
window.raise()?;
self.has_pending_raise_op = false;
}
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
}
WindowManagerEvent::Minimize(_, window) => {
let mut hide = false;
{
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
hide = true;
}
}
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
}
}
WindowManagerEvent::Hide(_, window) => {
let mut hide = false;
// Some major applications unfortunately send the HIDE signal when they are being
@@ -105,16 +140,17 @@ impl WindowManager {
// and will have is_window() return true, as the process is still running even if
// the window is not visible.
{
let tray_and_multi_window_exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
let tray_and_multi_window_classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
let tray_and_multi_window_identifiers =
TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
// We don't want to purge windows that have been deliberately hidden by us, eg. when
// they are not on the top of a container stack.
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if (!window.is_window() || tray_and_multi_window_exes.contains(&window.exe()?))
|| tray_and_multi_window_classes.contains(&window.class()?)
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
if ((!window.is_window()
|| tray_and_multi_window_identifiers.contains(&window.exe()?))
|| tray_and_multi_window_identifiers.contains(&window.class()?))
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
{
hide = true;
}
@@ -158,7 +194,7 @@ impl WindowManager {
if self.focused_monitor_idx() != known_monitor_idx
|| self
.focused_monitor()
.context("there is no monitor")?
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx()
!= known_workspace_idx
{
@@ -189,14 +225,53 @@ impl WindowManager {
}
}
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if !workspace.contains_window(window.hwnd) {
workspace.new_container_for_window(*window);
self.update_focused_workspace(false)?;
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(*window);
self.update_focused_workspace(false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(*window);
self.update_focused_workspace(true)?;
}
}
}
}
WindowManagerEvent::MoveResizeStart(_, _) => {
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();
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,
// that monitor will already have been focused and updated in the state
let pending = self.pending_move_op;
// Always consume the pending move op whenever this event is handled
self.pending_move_op = None;
let target_monitor_idx = self
.monitor_idx_from_current_pos()
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
let new_window_behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if workspace
.floating_windows()
@@ -206,26 +281,38 @@ impl WindowManager {
return Ok(());
}
let focused_idx = workspace.focused_container_idx();
let old_position = *workspace
.latest_layout()
.get(focused_idx)
.context("there is no latest layout")?;
let focused_container_idx = workspace.focused_container_idx();
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
// See Window.set_position() in window.rs for comments
let border = Rect {
left: 12,
top: 0,
right: 24,
bottom: 12,
};
let old_position = *workspace
.latest_layout()
.get(focused_container_idx)
// If the move was to another monitor with an empty workspace, the
// workspace here will refer to that empty workspace, which won't
// have any latest layout set. We fall back to a Default for Rect
// which allows us to make a reasonable guess that the drag has taken
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
// Adjust for the invisible border
new_position.left += border.left;
new_position.top += border.top;
new_position.right -= border.right;
new_position.bottom -= border.bottom;
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
if let Some((origin_monitor_idx, _, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
if !moved_across_monitors {
// So we'll check if the origin monitor index and the target monitor index
// are different, if they are, we can set the override
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
}
}
// 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,
@@ -234,16 +321,90 @@ impl WindowManager {
bottom: new_position.bottom - old_position.bottom,
};
let is_move = resize.right == 0 && resize.bottom == 0;
// If we have moved across the monitors, use that override, otherwise determine
// if a move has taken place by ruling out a resize
let is_move = moved_across_monitors || resize.right == 0 && resize.bottom == 0;
if is_move {
tracing::info!("moving with mouse");
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace.swap_containers(focused_idx, target_idx);
if moved_across_monitors {
if let Some((
origin_monitor_idx,
origin_workspace_idx,
origin_container_idx,
)) = pending
{
let target_workspace_idx = self
.monitors()
.get(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.focused_workspace_idx();
let target_container_idx = self
.monitors()
.get(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.focused_workspace()
.ok_or_else(|| {
anyhow!("there is no focused workspace for this monitor")
})?
.container_idx_from_current_point()
// Default to 0 in the case of an empty workspace
.unwrap_or(0);
self.transfer_container(
(
origin_monitor_idx,
origin_workspace_idx,
origin_container_idx,
),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
// We want to make sure both the origin and target monitors are updated,
// so that we don't have ghost tiles until we force an interaction on
// the origin monitor's focused workspace
self.focus_monitor(origin_monitor_idx)?;
self.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false)?;
self.focus_monitor(target_monitor_idx)?;
self.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false)?;
}
None => self.update_focused_workspace(true)?,
// Here we handle a simple move on the same monitor which is treated as
// a container swap
} else {
match new_window_behaviour {
WindowContainerBehaviour::Create => {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace
.swap_containers(focused_container_idx, target_idx);
self.update_focused_workspace(false)?;
}
None => {
self.update_focused_workspace(self.mouse_follows_focus)?;
}
}
}
WindowContainerBehaviour::Append => {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace.move_window_to_container(target_idx)?;
self.update_focused_workspace(false)?;
}
None => {
self.update_focused_workspace(self.mouse_follows_focus)?;
}
}
}
}
}
} else {
tracing::info!("resizing with mouse");
@@ -278,19 +439,19 @@ impl WindowManager {
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
}
for (edge, sizing, step) in ops {
self.resize_window(edge, sizing, Option::from(step))?;
for (edge, sizing, delta) in ops {
self.resize_window(edge, sizing, delta, true)?;
}
self.update_focused_workspace(false)?;
}
}
WindowManagerEvent::MouseCapture(..) => {}
WindowManagerEvent::MonitorPoll(..) | WindowManagerEvent::MouseCapture(..) => {}
};
// 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()?)?;
window.center(&self.focused_monitor_work_area()?, &invisible_borders)?;
}
tracing::trace!("updating list of known hwnds");
@@ -305,7 +466,8 @@ impl WindowManager {
}
}
let mut hwnd_json = dirs::home_dir().context("there is no home directory")?;
let mut hwnd_json =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
hwnd_json.push("komorebi.hwnd.json");
let file = OpenOptions::new()
.write(true)
@@ -314,6 +476,11 @@ impl WindowManager {
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &known_hwnds)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::WindowManager(*event),
state: (&*self).into(),
})?)?;
tracing::info!("processed: {}", event.window().to_string());
Ok(())
}

View File

@@ -0,0 +1,41 @@
use std::sync::Arc;
use parking_lot::Mutex;
use winput::message_loop;
use winput::message_loop::Event;
use winput::Action;
use komorebi_core::FocusFollowsMouseImplementation;
use crate::window_manager::WindowManager;
#[tracing::instrument]
pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
let mut ignore_movement = false;
let receiver = message_loop::start().expect("could not start winput message loop");
loop {
let focus_follows_mouse = wm.lock().focus_follows_mouse.clone();
if let Some(FocusFollowsMouseImplementation::Komorebi) = focus_follows_mouse {
match receiver.next_event() {
// Don't want to send any raise events while we are dragging or resizing
Event::MouseButton { action, .. } => match action {
Action::Press => ignore_movement = true,
Action::Release => ignore_movement = false,
},
Event::MouseMoveRelative { .. } => {
if !ignore_movement {
match wm.lock().raise_window_at_cursor_pos() {
Ok(_) => {}
Err(error) => tracing::error!("{}", error),
}
}
}
_ => {}
}
}
}
});
}

View File

@@ -1,38 +1,37 @@
use bitflags::bitflags;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_ASYNCWINDOWPOS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_DEFERERASE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_DRAWFRAME;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_FRAMECHANGED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_HIDEWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOCOPYBITS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOOWNERZORDER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOREDRAW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOREPOSITION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOSENDCHANGING;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOZORDER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
use windows::Win32::UI::WindowsAndMessaging::SWP_ASYNCWINDOWPOS;
use windows::Win32::UI::WindowsAndMessaging::SWP_DEFERERASE;
use windows::Win32::UI::WindowsAndMessaging::SWP_DRAWFRAME;
use windows::Win32::UI::WindowsAndMessaging::SWP_FRAMECHANGED;
use windows::Win32::UI::WindowsAndMessaging::SWP_HIDEWINDOW;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOCOPYBITS;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOOWNERZORDER;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOREDRAW;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOREPOSITION;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSENDCHANGING;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOZORDER;
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
bitflags! {
#[derive(Default)]
pub struct SetWindowPosition: u32 {
const ASYNC_WINDOW_POS = SWP_ASYNCWINDOWPOS.0;
const DEFER_ERASE = SWP_DEFERERASE.0;
const DRAW_FRAME = SWP_DRAWFRAME.0;
const FRAME_CHANGED = SWP_FRAMECHANGED.0;
const HIDE_WINDOW = SWP_HIDEWINDOW.0;
const NO_ACTIVATE = SWP_NOACTIVATE.0;
const NO_COPY_BITS = SWP_NOCOPYBITS.0;
const NO_MOVE = SWP_NOMOVE.0;
const NO_OWNER_Z_ORDER = SWP_NOOWNERZORDER.0;
const NO_REDRAW = SWP_NOREDRAW.0;
const NO_REPOSITION = SWP_NOREPOSITION.0;
const NO_SEND_CHANGING = SWP_NOSENDCHANGING.0;
const NO_SIZE = SWP_NOSIZE.0;
const NO_Z_ORDER = SWP_NOZORDER.0;
const SHOW_WINDOW = SWP_SHOWWINDOW.0;
const ASYNC_WINDOW_POS = SWP_ASYNCWINDOWPOS;
const DEFER_ERASE = SWP_DEFERERASE;
const DRAW_FRAME = SWP_DRAWFRAME;
const FRAME_CHANGED = SWP_FRAMECHANGED;
const HIDE_WINDOW = SWP_HIDEWINDOW;
const NO_ACTIVATE = SWP_NOACTIVATE;
const NO_COPY_BITS = SWP_NOCOPYBITS;
const NO_MOVE = SWP_NOMOVE;
const NO_OWNER_Z_ORDER = SWP_NOOWNERZORDER;
const NO_REDRAW = SWP_NOREDRAW;
const NO_REPOSITION = SWP_NOREPOSITION;
const NO_SEND_CHANGING = SWP_NOSENDCHANGING;
const NO_SIZE = SWP_NOSIZE;
const NO_Z_ORDER = SWP_NOZORDER;
const SHOW_WINDOW = SWP_SHOWWINDOW;
}
}

View File

@@ -1,122 +1,123 @@
use bitflags::bitflags;
use windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
use windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILDWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_CLIPCHILDREN;
use windows::Win32::UI::WindowsAndMessaging::WS_CLIPSIBLINGS;
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use windows::Win32::UI::WindowsAndMessaging::WS_DLGFRAME;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_ACCEPTFILES;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_APPWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_CLIENTEDGE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_COMPOSITED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTEXTHELP;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTROLPARENT;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_DLGMODALFRAME;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYOUTRTL;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFT;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFTSCROLLBAR;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LTRREADING;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_MDICHILD;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOINHERITLAYOUT;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOPARENTNOTIFY;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOREDIRECTIONBITMAP;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_OVERLAPPEDWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_PALETTEWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHT;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHTSCROLLBAR;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_RTLREADING;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_STATICEDGE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_WINDOWEDGE;
use windows::Win32::UI::WindowsAndMessaging::WS_GROUP;
use windows::Win32::UI::WindowsAndMessaging::WS_HSCROLL;
use windows::Win32::UI::WindowsAndMessaging::WS_ICONIC;
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZE;
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZE;
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
use windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPED;
use windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPEDWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUPWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_SIZEBOX;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use windows::Win32::UI::WindowsAndMessaging::WS_TABSTOP;
use windows::Win32::UI::WindowsAndMessaging::WS_THICKFRAME;
use windows::Win32::UI::WindowsAndMessaging::WS_TILED;
use windows::Win32::UI::WindowsAndMessaging::WS_TILEDWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
use windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPCHILDREN;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPSIBLINGS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DLGFRAME;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_ACCEPTFILES;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_APPWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CLIENTEDGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_COMPOSITED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTEXTHELP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTROLPARENT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_DLGMODALFRAME;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYOUTRTL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFTSCROLLBAR;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LTRREADING;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_MDICHILD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOINHERITLAYOUT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOPARENTNOTIFY;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOREDIRECTIONBITMAP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_OVERLAPPEDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_PALETTEWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHTSCROLLBAR;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RTLREADING;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_STATICEDGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_WINDOWEDGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_GROUP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_HSCROLL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_ICONIC;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPEDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUPWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SIZEBOX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TABSTOP;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_THICKFRAME;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILEDWINDOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
bitflags! {
#[derive(Default)]
pub struct GwlStyle: u32 {
const BORDER = WS_BORDER.0;
const CAPTION = WS_CAPTION.0;
const CHILD = WS_CHILD.0;
const CHILDWINDOW = WS_CHILDWINDOW.0;
const CLIPCHILDREN = WS_CLIPCHILDREN.0;
const CLIPSIBLINGS = WS_CLIPSIBLINGS.0;
const DISABLED = WS_DISABLED.0;
const DLGFRAME = WS_DLGFRAME.0;
const GROUP = WS_GROUP.0;
const HSCROLL = WS_HSCROLL.0;
const ICONIC = WS_ICONIC.0;
const MAXIMIZE = WS_MAXIMIZE.0;
const MAXIMIZEBOX = WS_MAXIMIZEBOX.0;
const MINIMIZE = WS_MINIMIZE.0;
const MINIMIZEBOX = WS_MINIMIZEBOX.0;
const OVERLAPPED = WS_OVERLAPPED.0;
const OVERLAPPEDWINDOW = WS_OVERLAPPEDWINDOW.0;
const POPUP = WS_POPUP.0;
const POPUPWINDOW = WS_POPUPWINDOW.0;
const SIZEBOX = WS_SIZEBOX.0;
const SYSMENU = WS_SYSMENU.0;
const TABSTOP = WS_TABSTOP.0;
const THICKFRAME = WS_THICKFRAME.0;
const TILED = WS_TILED.0;
const TILEDWINDOW = WS_TILEDWINDOW.0;
const VISIBLE = WS_VISIBLE.0;
const VSCROLL = WS_VSCROLL.0;
pub struct WindowStyle: u32 {
const BORDER = WS_BORDER;
const CAPTION = WS_CAPTION;
const CHILD = WS_CHILD;
const CHILDWINDOW = WS_CHILDWINDOW;
const CLIPCHILDREN = WS_CLIPCHILDREN;
const CLIPSIBLINGS = WS_CLIPSIBLINGS;
const DISABLED = WS_DISABLED;
const DLGFRAME = WS_DLGFRAME;
const GROUP = WS_GROUP;
const HSCROLL = WS_HSCROLL;
const ICONIC = WS_ICONIC;
const MAXIMIZE = WS_MAXIMIZE;
const MAXIMIZEBOX = WS_MAXIMIZEBOX;
const MINIMIZE = WS_MINIMIZE;
const MINIMIZEBOX = WS_MINIMIZEBOX;
const OVERLAPPED = WS_OVERLAPPED;
const OVERLAPPEDWINDOW = WS_OVERLAPPEDWINDOW;
const POPUP = WS_POPUP;
const POPUPWINDOW = WS_POPUPWINDOW;
const SIZEBOX = WS_SIZEBOX;
const SYSMENU = WS_SYSMENU;
const TABSTOP = WS_TABSTOP;
const THICKFRAME = WS_THICKFRAME;
const TILED = WS_TILED;
const TILEDWINDOW = WS_TILEDWINDOW;
const VISIBLE = WS_VISIBLE;
const VSCROLL = WS_VSCROLL;
}
}
// https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
bitflags! {
#[derive(Default)]
pub struct GwlExStyle: u32 {
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
const APPWINDOW = WS_EX_APPWINDOW.0;
const CLIENTEDGE = WS_EX_CLIENTEDGE.0;
const COMPOSITED = WS_EX_COMPOSITED.0;
const CONTEXTHELP = WS_EX_CONTEXTHELP.0;
const CONTROLPARENT = WS_EX_CONTROLPARENT.0;
const DLGMODALFRAME = WS_EX_DLGMODALFRAME.0;
const LAYERED = WS_EX_LAYERED.0;
const LAYOUTRTL = WS_EX_LAYOUTRTL.0;
const LEFT = WS_EX_LEFT.0;
const LEFTSCROLLBAR = WS_EX_LEFTSCROLLBAR.0;
const LTRREADING = WS_EX_LTRREADING.0;
const MDICHILD = WS_EX_MDICHILD.0;
const NOACTIVATE = WS_EX_NOACTIVATE.0;
const NOINHERITLAYOUT = WS_EX_NOINHERITLAYOUT.0;
const NOPARENTNOTIFY = WS_EX_NOPARENTNOTIFY.0;
const NOREDIRECTIONBITMAP = WS_EX_NOREDIRECTIONBITMAP.0;
const OVERLAPPEDWINDOW = WS_EX_OVERLAPPEDWINDOW.0;
const PALETTEWINDOW = WS_EX_PALETTEWINDOW.0;
const RIGHT = WS_EX_RIGHT.0;
const RIGHTSCROLLBAR = WS_EX_RIGHTSCROLLBAR.0;
const RTLREADING = WS_EX_RTLREADING.0;
const STATICEDGE = WS_EX_STATICEDGE.0;
const TOOLWINDOW = WS_EX_TOOLWINDOW.0;
const TOPMOST = WS_EX_TOPMOST.0;
const TRANSPARENT = WS_EX_TRANSPARENT.0;
const WINDOWEDGE = WS_EX_WINDOWEDGE.0;
pub struct ExtendedWindowStyle: u32 {
const ACCEPTFILES = WS_EX_ACCEPTFILES;
const APPWINDOW = WS_EX_APPWINDOW;
const CLIENTEDGE = WS_EX_CLIENTEDGE;
const COMPOSITED = WS_EX_COMPOSITED;
const CONTEXTHELP = WS_EX_CONTEXTHELP;
const CONTROLPARENT = WS_EX_CONTROLPARENT;
const DLGMODALFRAME = WS_EX_DLGMODALFRAME;
const LAYERED = WS_EX_LAYERED;
const LAYOUTRTL = WS_EX_LAYOUTRTL;
const LEFT = WS_EX_LEFT;
const LEFTSCROLLBAR = WS_EX_LEFTSCROLLBAR;
const LTRREADING = WS_EX_LTRREADING;
const MDICHILD = WS_EX_MDICHILD;
const NOACTIVATE = WS_EX_NOACTIVATE;
const NOINHERITLAYOUT = WS_EX_NOINHERITLAYOUT;
const NOPARENTNOTIFY = WS_EX_NOPARENTNOTIFY;
const NOREDIRECTIONBITMAP = WS_EX_NOREDIRECTIONBITMAP;
const OVERLAPPEDWINDOW = WS_EX_OVERLAPPEDWINDOW;
const PALETTEWINDOW = WS_EX_PALETTEWINDOW;
const RIGHT = WS_EX_RIGHT;
const RIGHTSCROLLBAR = WS_EX_RIGHTSCROLLBAR;
const RTLREADING = WS_EX_RTLREADING;
const STATICEDGE = WS_EX_STATICEDGE;
const TOOLWINDOW = WS_EX_TOOLWINDOW;
const TOPMOST = WS_EX_TOPMOST;
const TRANSPARENT = WS_EX_TRANSPARENT;
const WINDOWEDGE = WS_EX_WINDOWEDGE;
}
}

View File

@@ -2,23 +2,28 @@ use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use serde::ser::Error;
use serde::ser::SerializeStruct;
use serde::Serialize;
use serde::Serializer;
use windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::Foundation::HWND;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
use crate::styles::GwlExStyle;
use crate::styles::GwlStyle;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
use crate::LAYERED_EXE_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy)]
pub struct Window {
@@ -54,12 +59,28 @@ impl Serialize for Window {
{
let mut state = serializer.serialize_struct("Window", 5)?;
state.serialize_field("hwnd", &self.hwnd)?;
state.serialize_field("title", &self.title().expect("could not get window title"))?;
state.serialize_field("exe", &self.exe().expect("could not get window exe"))?;
state.serialize_field("class", &self.class().expect("could not get window class"))?;
state.serialize_field(
"title",
&self
.title()
.map_err(|_| S::Error::custom("could not get window title"))?,
)?;
state.serialize_field(
"exe",
&self
.exe()
.map_err(|_| S::Error::custom("could not get window exe"))?,
)?;
state.serialize_field(
"class",
&self
.class()
.map_err(|_| S::Error::custom("could not get window class"))?,
)?;
state.serialize_field(
"rect",
&WindowsApi::window_rect(self.hwnd()).expect("could not get window rect"),
&WindowsApi::window_rect(self.hwnd())
.map_err(|_| S::Error::custom("could not get window rect"))?,
)?;
state.end()
}
@@ -70,7 +91,7 @@ impl Window {
HWND(self.hwnd)
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
pub fn center(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
@@ -81,44 +102,35 @@ impl Window {
right: half_width,
bottom: half_weight,
},
invisible_borders,
true,
)
}
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
// NOTE: This is how the border variable below was calculated; every time this code was
// run on any window in any position, the generated border was always the same, so I am
// hard coding the border Rect to avoid two calls to set_window_pos and making the screen
// flicker on container/window movement. Still not 100% sure if this is DPI-aware.
// Set the new position first to be able to get the extended frame bounds
// WindowsApi::set_window_pos(self.hwnd(), layout, false, false)?;
// let mut rect = WindowsApi::window_rect(self.hwnd())?;
// Get the extended frame bounds of the new position
// let frame = WindowsApi::window_rect_with_extended_frame_bounds(self.hwnd())?;
// Calculate the invisible border diff
// let border = Rect {
// left: frame.left - rect.left,
// top: frame.top - rect.top,
// right: rect.right - frame.right,
// bottom: rect.bottom - frame.bottom,
// };
pub fn set_position(
&mut self,
layout: &Rect,
invisible_borders: &Rect,
top: bool,
) -> Result<()> {
let mut rect = *layout;
let border = Rect {
left: 12,
top: 0,
right: 24,
bottom: 12,
};
let mut should_remove_border = true;
// Remove the invisible border
rect.left -= border.left;
rect.top -= border.top;
rect.right += border.right;
rect.bottom += border.bottom;
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
if border_overflows.contains(&self.title()?)
|| border_overflows.contains(&self.exe()?)
|| border_overflows.contains(&self.class()?)
{
should_remove_border = false;
}
if should_remove_border {
// Remove the invisible borders
rect.left -= invisible_borders.left;
rect.top -= invisible_borders.top;
rect.right += invisible_borders.right;
rect.bottom += invisible_borders.bottom;
}
WindowsApi::position_window(self.hwnd(), &rect, top)
}
@@ -129,7 +141,11 @@ impl Window {
programmatically_hidden_hwnds.push(self.hwnd);
}
WindowsApi::hide_window(self.hwnd());
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
match *hiding_behaviour {
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd()),
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd()),
}
}
pub fn restore(self) {
@@ -156,7 +172,28 @@ impl Window {
WindowsApi::maximize_window(self.hwnd());
}
pub fn focus(self) -> Result<()> {
pub fn raise(self) -> Result<()> {
// 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();
WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true)?;
// 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 focus(): {}",
error
);
}
};
// This isn't really needed when the above command works as expected via AHK
WindowsApi::set_focus(self.hwnd())
}
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());
let current_thread_id = WindowsApi::current_thread_id();
@@ -174,25 +211,27 @@ impl Window {
};
// Center cursor in Window
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
}
// This isn't really needed when the above command works as expected via AHK
WindowsApi::set_focus(self.hwnd())
}
#[allow(dead_code)]
pub fn update_style(self, style: GwlStyle) -> Result<()> {
pub fn update_style(self, style: WindowStyle) -> Result<()> {
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
}
pub fn style(self) -> Result<GwlStyle> {
pub fn style(self) -> Result<WindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
GwlStyle::from_bits(bits).context("there is no gwl style")
WindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
}
pub fn ex_style(self) -> Result<GwlExStyle> {
pub fn ex_style(self) -> Result<ExtendedWindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
GwlExStyle::from_bits(bits).context("there is no gwl style")
ExtendedWindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
}
pub fn title(self) -> Result<String> {
@@ -218,6 +257,11 @@ impl Window {
#[tracing::instrument(fields(exe, title))]
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
if let Some(WindowManagerEvent::MonitorPoll(_, _)) = event {
return Ok(true);
}
#[allow(clippy::question_mark)]
if self.title().is_err() {
return Ok(false);
}
@@ -254,17 +298,21 @@ impl Window {
layered_exe_whitelist.contains(&exe_name)
};
let allow_wsl2_gui = {
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
wsl2_ui_processes.contains(&exe_name)
};
let style = self.style()?;
let ex_style = self.ex_style()?;
if style.contains(GwlStyle::CAPTION)
&& ex_style.contains(GwlExStyle::WINDOWEDGE)
&& !ex_style.contains(GwlExStyle::DLGMODALFRAME)
if (allow_wsl2_gui || 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
// on FocusChange events if I don't filter out this one. But, if we are
// allowing a specific layered window on the whitelist (like Steam), it should
// pass this check
&& (allow_layered || !ex_style.contains(GwlExStyle::LAYERED))
&& (allow_layered || !ex_style.contains(ExtendedWindowStyle::LAYERED))
|| managed_override
{
return Ok(true);

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,27 @@
use std::fmt::Display;
use std::fmt::Formatter;
use serde::Serialize;
use crate::window::Window;
use crate::winevent::WinEvent;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(tag = "type", content = "content")]
pub enum WindowManagerEvent {
Destroy(WinEvent, Window),
FocusChange(WinEvent, Window),
Hide(WinEvent, Window),
Minimize(WinEvent, Window),
Show(WinEvent, Window),
MoveResizeStart(WinEvent, Window),
MoveResizeEnd(WinEvent, Window),
MouseCapture(WinEvent, Window),
Manage(Window),
Unmanage(Window),
Raise(Window),
MonitorPoll(WinEvent, Window),
}
impl Display for WindowManagerEvent {
@@ -46,6 +52,13 @@ impl Display for WindowManagerEvent {
WindowManagerEvent::Show(winevent, window) => {
write!(f, "Show (WinEvent: {}, Window: {})", winevent, window)
}
WindowManagerEvent::MoveResizeStart(winevent, window) => {
write!(
f,
"MoveResizeStart (WinEvent: {}, Window: {})",
winevent, window
)
}
WindowManagerEvent::MoveResizeEnd(winevent, window) => {
write!(
f,
@@ -60,6 +73,16 @@ impl Display for WindowManagerEvent {
winevent, window
)
}
WindowManagerEvent::Raise(window) => {
write!(f, "Raise (Window: {})", window)
}
WindowManagerEvent::MonitorPoll(winevent, window) => {
write!(
f,
"MonitorPoll (WinEvent: {}, Window: {})",
winevent, window
)
}
}
}
}
@@ -72,8 +95,11 @@ impl WindowManagerEvent {
| WindowManagerEvent::Hide(_, window)
| WindowManagerEvent::Minimize(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::MoveResizeStart(_, window)
| WindowManagerEvent::MoveResizeEnd(_, window)
| WindowManagerEvent::MouseCapture(_, window)
| WindowManagerEvent::MonitorPoll(_, window)
| WindowManagerEvent::Raise(window)
| WindowManagerEvent::Manage(window)
| WindowManagerEvent::Unmanage(window) => window,
}
@@ -96,6 +122,7 @@ impl WindowManagerEvent {
WinEvent::ObjectFocus | WinEvent::SystemForeground => {
Option::from(Self::FocusChange(winevent, window))
}
WinEvent::SystemMoveSizeStart => Option::from(Self::MoveResizeStart(winevent, window)),
WinEvent::SystemMoveSizeEnd => Option::from(Self::MoveResizeEnd(winevent, window)),
WinEvent::SystemCaptureStart | WinEvent::SystemCaptureEnd => {
Option::from(Self::MouseCapture(winevent, window))
@@ -116,6 +143,17 @@ impl WindowManagerEvent {
None
}
}
WinEvent::ObjectCreate => {
if let Ok(title) = window.title() {
// Hidden COM support mechanism window that fires this event on both DPI/scaling
// changes and resolution changes, a good candidate for polling
if title == "OLEChannelWnd" {
return Option::from(Self::MonitorPoll(winevent, window));
}
}
None
}
_ => None,
}
}

View File

@@ -3,78 +3,82 @@ use std::convert::TryFrom;
use std::convert::TryInto;
use std::ffi::c_void;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::Error;
use color_eyre::Result;
use eyre::Error;
use windows::core::Result as WindowsCrateResult;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::POINT;
use windows::Win32::Foundation::PWSTR;
use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
use windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use windows::Win32::Graphics::Gdi::MonitorFromPoint;
use windows::Win32::Graphics::Gdi::MonitorFromWindow;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use windows::Win32::Graphics::Gdi::MONITORINFO;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
use windows::Win32::System::Threading::AttachThreadInput;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::System::Threading::OpenProcess;
use windows::Win32::System::Threading::QueryFullProcessImageNameW;
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
use windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
use windows::Win32::UI::WindowsAndMessaging::GetWindowTextW;
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::RealGetWindowClassW;
use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION;
use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use bindings::Windows::Win32::Foundation::BOOL;
use bindings::Windows::Win32::Foundation::HANDLE;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::Foundation::LPARAM;
use bindings::Windows::Win32::Foundation::POINT;
use bindings::Windows::Win32::Foundation::PWSTR;
use bindings::Windows::Win32::Foundation::RECT;
use bindings::Windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use bindings::Windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use bindings::Windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
use bindings::Windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
use bindings::Windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use bindings::Windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use bindings::Windows::Win32::Graphics::Gdi::MonitorFromWindow;
use bindings::Windows::Win32::Graphics::Gdi::HDC;
use bindings::Windows::Win32::Graphics::Gdi::HMONITOR;
use bindings::Windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use bindings::Windows::Win32::Graphics::Gdi::MONITORINFO;
use bindings::Windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use bindings::Windows::Win32::System::Threading::AttachThreadInput;
use bindings::Windows::Win32::System::Threading::GetCurrentProcessId;
use bindings::Windows::Win32::System::Threading::GetCurrentThreadId;
use bindings::Windows::Win32::System::Threading::OpenProcess;
use bindings::Windows::Win32::System::Threading::QueryFullProcessImageNameW;
use bindings::Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
use bindings::Windows::Win32::System::Threading::PROCESS_NAME_FORMAT;
use bindings::Windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::SetFocus;
use bindings::Windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowTextW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsIconic;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
use bindings::Windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use komorebi_core::Rect;
use crate::container::Container;
@@ -83,43 +87,12 @@ use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::workspace::Workspace;
pub enum WindowsResult<T, E> {
Err(E),
Ok(T),
}
impl From<BOOL> for WindowsResult<(), Error> {
fn from(return_value: BOOL) -> Self {
if return_value.as_bool() {
Self::Ok(())
} else {
Self::Err(std::io::Error::last_os_error().into())
}
}
}
impl From<HWND> for WindowsResult<isize, Error> {
fn from(return_value: HWND) -> Self {
if return_value.is_null() {
Self::Err(std::io::Error::last_os_error().into())
} else {
Self::Ok(return_value.0)
}
}
}
impl From<HANDLE> for WindowsResult<HANDLE, Error> {
fn from(return_value: HANDLE) -> Self {
if return_value.is_null() {
Self::Err(std::io::Error::last_os_error().into())
} else {
Self::Ok(return_value)
}
}
}
macro_rules! impl_from_integer_for_windows_result {
( $( $integer_type:ty ),+ ) => {
$(
@@ -146,6 +119,40 @@ impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
}
}
pub trait ProcessWindowsCrateResult<T> {
fn process(self) -> Result<T>;
}
macro_rules! impl_process_windows_crate_result {
( $($input:ty => $deref:ty),+ $(,)? ) => (
paste::paste! {
$(
impl ProcessWindowsCrateResult<$deref> for WindowsCrateResult<$input> {
fn process(self) -> Result<$deref> {
match self {
Ok(value) => Ok(value.0),
Err(error) => Err(error.into()),
}
}
}
)+
}
);
}
impl_process_windows_crate_result!(
HWND => isize,
);
impl<T> ProcessWindowsCrateResult<T> for WindowsCrateResult<T> {
fn process(self) -> Result<T> {
match self {
Ok(value) => Ok(value),
Err(error) => Err(error.into()),
}
}
}
pub struct WindowsApi;
impl WindowsApi {
@@ -153,37 +160,49 @@ impl WindowsApi {
callback: MONITORENUMPROC,
callback_data_address: isize,
) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
unsafe {
EnumDisplayMonitors(
HDC(0),
std::ptr::null_mut(),
Option::from(callback),
callback,
LPARAM(callback_data_address),
)
}))
}
.ok()
.process()
}
pub fn valid_hmonitors() -> Result<Vec<isize>> {
let mut monitors: Vec<isize> = vec![];
let monitors_ref: &mut Vec<isize> = monitors.as_mut();
Self::enum_display_monitors(
Option::Some(windows_callbacks::valid_display_monitors),
monitors_ref as *mut Vec<isize> as isize,
)?;
Ok(monitors)
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
Self::enum_display_monitors(
windows_callbacks::enum_display_monitor,
Option::Some(windows_callbacks::enum_display_monitor),
monitors as *mut Ring<Monitor> as isize,
)
}
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
EnumWindows(Option::from(callback), LPARAM(callback_data_address))
}))
unsafe { EnumWindows(callback, LPARAM(callback_data_address)) }
.ok()
.process()
}
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
for monitor in monitors.elements_mut() {
if monitor.workspaces().is_empty() {
let mut workspace = Workspace::default();
let monitor_id = monitor.id();
if let Some(workspace) = monitor.workspaces_mut().front_mut() {
// EnumWindows will enumerate through windows on all monitors
Self::enum_windows(
windows_callbacks::enum_window,
Option::Some(windows_callbacks::enum_window),
workspace.containers_mut() as *mut VecDeque<Container> as isize,
)?;
@@ -197,7 +216,7 @@ impl WindowsApi {
for container in workspace.containers_mut() {
for window in container.windows() {
if Self::monitor_from_window(window.hwnd()) != monitor.id() {
if Self::monitor_from_window(window.hwnd()) != monitor_id {
windows_on_other_monitors.push(window.hwnd().0);
}
}
@@ -206,8 +225,6 @@ impl WindowsApi {
for hwnd in windows_on_other_monitors {
workspace.remove_window(hwnd)?;
}
monitor.workspaces_mut().push_back(workspace);
}
}
@@ -215,9 +232,9 @@ impl WindowsApi {
}
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
AllowSetForegroundWindow(process_id)
}))
unsafe { AllowSetForegroundWindow(process_id) }
.ok()
.process()
}
pub fn monitor_from_window(hwnd: HWND) -> isize {
@@ -226,6 +243,12 @@ impl WindowsApi {
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
}
pub fn monitor_from_point(point: POINT) -> isize {
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
}
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
let flags = SetWindowPosition::NO_ACTIVATE;
@@ -234,7 +257,7 @@ impl WindowsApi {
}
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
unsafe {
SetWindowPos(
hwnd,
position,
@@ -242,9 +265,11 @@ impl WindowsApi {
layout.top,
layout.right,
layout.bottom,
SET_WINDOW_POS_FLAGS(flags),
flags,
)
}))
}
.ok()
.process()
}
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
@@ -253,6 +278,10 @@ impl WindowsApi {
unsafe { ShowWindow(hwnd, command) };
}
pub fn minimize_window(hwnd: HWND) {
Self::show_window(hwnd, SW_MINIMIZE);
}
pub fn hide_window(hwnd: HWND) {
Self::show_window(hwnd, SW_HIDE);
}
@@ -266,39 +295,25 @@ impl WindowsApi {
}
pub fn foreground_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetForegroundWindow() }))
unsafe { GetForegroundWindow() }.ok().process()
}
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) {
WindowsResult::Ok(_) => Ok(()),
WindowsResult::Err(error) => {
// TODO: Figure out the odd behaviour here, docs state that a zero value means
// TODO: that the window was not brought to the foreground, but this contradicts
// TODO: the behaviour that I have observed which resulted in this check
if error.to_string() == "The operation completed successfully. (os error 0)" {
Ok(())
} else {
Err(error)
}
}
}
unsafe { SetForegroundWindow(hwnd) }.ok().process()
}
#[allow(dead_code)]
pub fn top_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetTopWindow(HWND::NULL).0 }))
unsafe { GetTopWindow(HWND::default()) }.ok().process()
}
pub fn desktop_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetDesktopWindow() }))
unsafe { GetDesktopWindow() }.ok().process()
}
#[allow(dead_code)]
pub fn next_window(hwnd: HWND) -> Result<isize> {
Result::from(WindowsResult::from(unsafe {
GetWindow(hwnd, GW_HWNDNEXT).0
}))
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok().process()
}
#[allow(dead_code)]
@@ -314,33 +329,35 @@ impl WindowsApi {
next_hwnd = Self::next_window(HWND(next_hwnd))?;
}
Err(eyre::anyhow!("could not find next window"))
Err(anyhow!("could not find next window"))
}
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
let mut rect = unsafe { std::mem::zeroed() };
Result::from(WindowsResult::from(unsafe {
GetWindowRect(hwnd, &mut rect)
}))?;
unsafe { GetWindowRect(hwnd, &mut rect) }.ok().process()?;
Ok(Rect::from(rect))
}
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
Result::from(WindowsResult::from(unsafe { SetCursorPos(x, y) }))
unsafe { SetCursorPos(x, y) }.ok().process()
}
pub fn cursor_pos() -> Result<POINT> {
let mut cursor_pos: POINT = unsafe { std::mem::zeroed() };
Result::from(WindowsResult::from(unsafe {
GetCursorPos(&mut cursor_pos)
}))?;
let mut cursor_pos = POINT::default();
unsafe { GetCursorPos(&mut cursor_pos) }.ok().process()?;
Ok(cursor_pos)
}
pub fn window_from_point(point: POINT) -> Result<isize> {
unsafe { WindowFromPoint(point) }.ok().process()
}
pub fn window_at_cursor_pos() -> Result<isize> {
Self::window_from_point(Self::cursor_pos()?)
}
pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> {
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
}
@@ -363,25 +380,27 @@ impl WindowsApi {
unsafe { GetCurrentProcessId() }
}
pub fn process_id_to_session_id() -> Result<u32> {
let process_id = Self::current_process_id();
let mut session_id = 0;
unsafe {
if ProcessIdToSessionId(process_id, &mut session_id).as_bool() {
Ok(session_id)
} else {
Err(anyhow!("could not determine current session id"))
}
}
}
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
AttachThreadInput(thread_id, target_thread_id, attach)
}))
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
.ok()
.process()
}
pub fn set_focus(hwnd: HWND) -> Result<()> {
match WindowsResult::from(unsafe { SetFocus(hwnd) }) {
WindowsResult::Ok(_) => Ok(()),
WindowsResult::Err(error) => {
// If the window is not attached to the calling thread's message queue, the return value is NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setfocus
if error.to_string() == "The operation completed successfully. (os error 0)" {
Ok(())
} else {
Err(error)
}
}
}
unsafe { SetFocus(hwnd) }.ok().map(|_| ()).process()
}
#[allow(dead_code)]
@@ -433,9 +452,9 @@ impl WindowsApi {
inherit_handle: bool,
process_id: u32,
) -> Result<HANDLE> {
Result::from(WindowsResult::from(unsafe {
OpenProcess(access_rights, inherit_handle, process_id)
}))
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }
.ok()
.process()
}
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
@@ -447,14 +466,11 @@ impl WindowsApi {
let mut path: Vec<u16> = vec![0; len as usize];
let text_ptr = path.as_mut_ptr();
Result::from(WindowsResult::from(unsafe {
QueryFullProcessImageNameW(
handle,
PROCESS_NAME_FORMAT(0),
PWSTR(text_ptr),
&mut len as *mut u32,
)
}))?;
unsafe {
QueryFullProcessImageNameW(handle, 0, PWSTR(text_ptr), std::ptr::addr_of_mut!(len))
}
.ok()
.process()?;
Ok(String::from_utf16(&path[..len as usize])?)
}
@@ -463,7 +479,7 @@ impl WindowsApi {
Ok(Self::exe_path(handle)?
.split('\\')
.last()
.context("there is no last element")?
.ok_or_else(|| anyhow!("there is no last element"))?
.to_string())
}
@@ -486,7 +502,7 @@ impl WindowsApi {
unsafe {
DwmGetWindowAttribute(
hwnd,
std::mem::transmute::<_, u32>(attribute),
attribute,
(value as *mut T).cast(),
u32::try_from(std::mem::size_of::<T>())?,
)?;
@@ -529,34 +545,50 @@ impl WindowsApi {
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
Result::from(WindowsResult::from(unsafe {
GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast())
}))?;
unsafe { GetMonitorInfoW(hmonitor, std::ptr::addr_of_mut!(monitor_info).cast()) }
.ok()
.process()?;
Ok(monitor_info)
}
pub fn monitor(hmonitor: HMONITOR) -> Result<Monitor> {
let monitor_info = Self::monitor_info_w(hmonitor)?;
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
let monitor_info = Self::monitor_info_w(HMONITOR(hmonitor))?;
Ok(monitor::new(
hmonitor.0,
hmonitor,
monitor_info.rcMonitor.into(),
monitor_info.rcWork.into(),
))
}
#[allow(dead_code)]
pub fn system_parameters_info_w(
action: SYSTEM_PARAMETERS_INFO_ACTION,
ui_param: u32,
pv_param: *mut c_void,
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
SystemParametersInfoW(action, ui_param, pv_param, update_flags)
}))
unsafe { SystemParametersInfoW(action, ui_param, pv_param, update_flags) }
.ok()
.process()
}
#[allow(dead_code)]
pub fn focus_follows_mouse() -> Result<bool> {
let mut is_enabled: BOOL = unsafe { std::mem::zeroed() };
Self::system_parameters_info_w(
SPI_GETACTIVEWINDOWTRACKING,
0,
std::ptr::addr_of_mut!(is_enabled).cast(),
SPIF_SENDCHANGE,
)?;
Ok(is_enabled.into())
}
#[allow(dead_code)]
pub fn enable_focus_follows_mouse() -> Result<()> {
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,
@@ -566,6 +598,7 @@ impl WindowsApi {
)
}
#[allow(dead_code)]
pub fn disable_focus_follows_mouse() -> Result<()> {
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,

View File

@@ -1,12 +1,12 @@
use std::collections::VecDeque;
use bindings::Windows::Win32::Foundation::BOOL;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::Foundation::LPARAM;
use bindings::Windows::Win32::Foundation::RECT;
use bindings::Windows::Win32::Graphics::Gdi::HDC;
use bindings::Windows::Win32::Graphics::Gdi::HMONITOR;
use bindings::Windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use crate::container::Container;
use crate::monitor::Monitor;
@@ -16,6 +16,17 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
monitors.push(hmonitor.0);
true.into()
}
pub extern "system" fn enum_display_monitor(
hmonitor: HMONITOR,
_: HDC,
@@ -23,7 +34,15 @@ pub extern "system" fn enum_display_monitor(
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
if let Ok(m) = WindowsApi::monitor(hmonitor) {
// Don't duplicate a monitor that is already being managed
for monitor in monitors.elements() {
if monitor.id() == hmonitor.0 {
return true.into();
}
}
if let Ok(m) = WindowsApi::monitor(hmonitor.0) {
monitors.elements_mut().push_back(m);
}

View File

@@ -1,91 +1,91 @@
use serde::Serialize;
use strum::Display;
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_START;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_CARET;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END_APPLICATION;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_LAYOUT;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_START_APPLICATION;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_REGION;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SCROLL;
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SIMPLE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_ACCELERATORCHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CLOAKED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CONTENTSCROLLED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CREATE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DEFACTIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESCRIPTIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCANCEL;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCOMPLETE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGDROPPED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGENTER;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGLEAVE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_FOCUS;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HELPCHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HIDE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_CHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_HIDE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_SHOW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_INVOKED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LIVEREGIONCHANGED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_NAMECHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_PARENTCHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_REORDER;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTION;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONADD;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONREMOVE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONWITHIN;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SHOW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_STATECHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTSELECTIONCHANGED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_UNCLOAKED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_VALUECHANGE;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_START;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ALERT;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ARRANGMENTPREVIEW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTUREEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTURESTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DESKTOPSWITCH;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_FOREGROUND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_IME_KEY_NOTIFICATION;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZEEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZESTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZEEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZESTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SOUND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHEND;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPDROPPED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPGRABBED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPOVERTARGET;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_CANCELLED;
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHSTART;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_END;
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;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_START;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_CARET;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END_APPLICATION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_LAYOUT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_START_APPLICATION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_REGION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SCROLL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SIMPLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_ACCELERATORCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CLOAKED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CONTENTSCROLLED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CREATE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DEFACTIONCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESCRIPTIONCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCANCEL;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCOMPLETE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGDROPPED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGENTER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGLEAVE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_FOCUS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HELPCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HIDE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_CHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_HIDE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_SHOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_INVOKED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LIVEREGIONCHANGED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_NAMECHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_PARENTCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_REORDER;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONADD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONREMOVE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONWITHIN;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SHOW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_STATECHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTSELECTIONCHANGED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_UNCLOAKED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_VALUECHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_START;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ALERT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ARRANGMENTPREVIEW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTUREEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTURESTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DESKTOPSWITCH;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_FOREGROUND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_IME_KEY_NOTIFICATION;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZEEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZESTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZEEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZESTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SOUND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHEND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPDROPPED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPGRABBED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPOVERTARGET;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_CANCELLED;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHSTART;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
#[derive(Clone, Copy, PartialEq, Debug, Display)]
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Display)]
#[repr(u32)]
#[allow(dead_code)]
pub enum WinEvent {

View File

@@ -8,16 +8,15 @@ use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::UI::Accessibility::SetWinEventHook;
use bindings::Windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
use bindings::Windows::Win32::UI::WindowsAndMessaging::MSG;
use bindings::Windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
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::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;

View File

@@ -1,7 +1,7 @@
use std::collections::VecDeque;
use std::num::NonZeroUsize;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use getset::CopyGetters;
use getset::Getters;
@@ -9,7 +9,9 @@ use getset::MutGetters;
use getset::Setters;
use serde::Serialize;
use komorebi_core::Flip;
use komorebi_core::Axis;
use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
@@ -36,10 +38,10 @@ pub struct Workspace {
maximized_window_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub")]
floating_windows: Vec<Window>,
#[getset(get_copy = "pub", set = "pub")]
#[getset(get = "pub", get_mut = "pub", set = "pub")]
layout: Layout,
#[getset(get_copy = "pub", set = "pub")]
layout_flip: Option<Flip>,
layout_flip: Option<Axis>,
#[getset(get_copy = "pub", set = "pub")]
workspace_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
@@ -47,8 +49,7 @@ pub struct Workspace {
#[serde(skip_serializing)]
#[getset(get = "pub", set = "pub")]
latest_layout: Vec<Rect>,
#[serde(skip_serializing)]
#[getset(get = "pub", get_mut = "pub")]
#[getset(get = "pub", get_mut = "pub", set = "pub")]
resize_dimensions: Vec<Option<Rect>>,
#[getset(get = "pub", set = "pub")]
tile: bool,
@@ -66,7 +67,7 @@ impl Default for Workspace {
maximized_window_restore_idx: None,
monocle_container_restore_idx: None,
floating_windows: Vec::default(),
layout: Layout::BSP,
layout: Layout::Default(DefaultLayout::BSP),
layout_flip: None,
workspace_padding: Option::from(10),
container_padding: Option::from(10),
@@ -100,7 +101,7 @@ impl Workspace {
}
}
pub fn restore(&mut self) -> Result<()> {
pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> {
let idx = self.focused_container_idx();
let mut to_focus = None;
for (i, container) in self.containers_mut().iter_mut().enumerate() {
@@ -131,15 +132,33 @@ impl Workspace {
// Maximised windows should always be drawn at the top of the Z order
if let Some(window) = to_focus {
if self.maximized_window().is_none() {
window.focus()?;
window.focus(mouse_follows_focus)?;
}
}
Ok(())
}
pub fn update(&mut self, work_area: &Rect) -> Result<()> {
let mut adjusted_work_area = *work_area;
pub fn update(
&mut self,
work_area: &Rect,
offset: Option<Rect>,
invisible_borders: &Rect,
) -> Result<()> {
let container_padding = self.container_padding();
let mut adjusted_work_area = offset.map_or_else(
|| *work_area,
|offset| {
let mut with_offset = *work_area;
with_offset.left += offset.left;
with_offset.top += offset.top;
with_offset.right -= offset.right;
with_offset.bottom -= offset.bottom;
with_offset
},
);
adjusted_work_area.add_padding(self.workspace_padding());
self.enforce_resize_constraints();
@@ -147,16 +166,19 @@ impl Workspace {
if *self.tile() {
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window_mut() {
window.set_position(&adjusted_work_area, true)?;
adjusted_work_area.add_padding(container_padding);
window.set_position(&adjusted_work_area, invisible_borders, true)?;
};
} else if let Some(window) = self.maximized_window_mut() {
window.maximize();
} else if !self.containers().is_empty() {
let layouts = self.layout().calculate(
let layouts = self.layout().as_boxed_arrangement().calculate(
&adjusted_work_area,
NonZeroUsize::new(self.containers().len()).context(
"there must be at least one container to calculate a workspace layout",
)?,
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
anyhow!(
"there must be at least one container to calculate a workspace layout"
)
})?,
self.container_padding(),
self.layout_flip(),
self.resize_dimensions(),
@@ -165,7 +187,7 @@ impl Workspace {
let windows = self.visible_windows_mut();
for (i, window) in windows.into_iter().enumerate() {
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
window.set_position(layout, false)?;
window.set_position(layout, invisible_borders, false)?;
}
}
@@ -229,16 +251,16 @@ impl Workspace {
pub fn focus_container_by_window(&mut self, hwnd: isize) -> Result<()> {
let container_idx = self
.container_idx_for_window(hwnd)
.context("there is no container/window")?;
.ok_or_else(|| anyhow!("there is no container/window"))?;
let container = self
.containers_mut()
.get_mut(container_idx)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window_idx = container
.idx_for_window(hwnd)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
container.focus_window(window_idx);
self.focus_container(container_idx);
@@ -262,6 +284,38 @@ impl Workspace {
idx
}
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
for container in self.containers() {
if let Some(hwnd) = container.hwnd_from_exe(exe) {
return Option::from(hwnd);
}
}
if let Some(window) = self.maximized_window() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(window.hwnd);
}
}
}
if let Some(container) = self.monocle_container() {
if let Some(hwnd) = container.hwnd_from_exe(exe) {
return Option::from(hwnd);
}
}
for window in self.floating_windows() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(window.hwnd);
}
}
}
None
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {
@@ -291,12 +345,24 @@ impl Workspace {
}
pub fn promote_container(&mut self) -> Result<()> {
let resize = self.resize_dimensions_mut().remove(0);
let container = self
.remove_focused_container()
.context("there is no container")?;
self.containers_mut().push_front(container);
self.resize_dimensions_mut().insert(0, None);
self.focus_container(0);
.ok_or_else(|| anyhow!("there is no container"))?;
let primary_idx = match self.layout() {
Layout::Default(_) => 0,
Layout::Custom(layout) => layout.first_container_idx(
layout
.primary_idx()
.ok_or_else(|| anyhow!("this custom layout does not have a primary column"))?,
),
};
self.containers_mut().insert(primary_idx, container);
self.resize_dimensions_mut().insert(primary_idx, resize);
self.focus_container(primary_idx);
Ok(())
}
@@ -307,8 +373,15 @@ impl Workspace {
}
fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
self.resize_dimensions_mut().remove(idx);
self.containers_mut().remove(idx)
if idx < self.resize_dimensions().len() {
self.resize_dimensions_mut().remove(idx);
}
if idx < self.containers().len() {
return self.containers_mut().remove(idx);
}
None
}
fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {
@@ -328,37 +401,66 @@ impl Workspace {
return Ok(());
}
if let Some(container) = self.monocle_container_mut() {
if let Some(window_idx) = container
.windows()
.iter()
.position(|window| window.hwnd == hwnd)
{
container
.remove_window_by_idx(window_idx)
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.set_monocle_container(None);
self.set_monocle_container_restore_idx(None);
}
return Ok(());
}
}
if let Some(window) = self.maximized_window() {
if window.hwnd == hwnd {
self.set_maximized_window(None);
self.set_maximized_window_restore_idx(None);
return Ok(());
}
}
let container_idx = self
.container_idx_for_window(hwnd)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
let container = self
.containers_mut()
.get_mut(container_idx)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window_idx = container
.windows()
.iter()
.position(|window| window.hwnd == hwnd)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
container
.remove_window_by_idx(window_idx)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.containers_mut()
.remove(container_idx)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
// Whenever a container is empty, we need to remove any resize dimensions for it too
if self.resize_dimensions().get(container_idx).is_some() {
self.resize_dimensions_mut().remove(container_idx);
}
}
self.focus_previous_container();
self.focus_previous_container();
} else {
container.load_focused_window();
}
Ok(())
}
@@ -371,21 +473,28 @@ impl Workspace {
container
}
pub fn remove_container(&mut self, idx: usize) -> Option<Container> {
let container = self.remove_container_by_idx(idx);
self.focus_previous_container();
container
}
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
if direction.is_valid(
self.layout(),
let len = NonZeroUsize::new(self.containers().len())?;
direction.destination(
self.layout().as_boxed_direction().as_ref(),
self.layout_flip(),
self.focused_container_idx(),
self.containers().len(),
) {
Option::from(direction.new_idx(
self.layout(),
self.layout_flip(),
self.containers.focused_idx(),
))
} else {
None
}
len,
)
}
pub fn new_idx_for_cycle_direction(&self, direction: CycleDirection) -> Option<usize> {
Option::from(direction.next_idx(
self.focused_container_idx(),
NonZeroUsize::new(self.containers().len())?,
))
}
pub fn move_window_to_container(&mut self, target_container_idx: usize) -> Result<()> {
@@ -393,11 +502,11 @@ impl Workspace {
let container = self
.focused_container_mut()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
// This is a little messy
let adjusted_target_container_index = if container.windows().is_empty() {
@@ -417,13 +526,13 @@ impl Workspace {
let target_container = self
.containers_mut()
.get_mut(adjusted_target_container_index)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
target_container.add_window(window);
self.focus_container(adjusted_target_container_index);
self.focused_container_mut()
.context("there is no container")?
.ok_or_else(|| anyhow!("there is no container"))?
.load_focused_window();
Ok(())
@@ -434,11 +543,11 @@ impl Workspace {
let container = self
.focused_container_mut()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.containers_mut().remove(focused_container_idx);
@@ -458,7 +567,7 @@ impl Workspace {
let focused_idx = self.focused_container_idx();
let window = self
.remove_focused_floating_window()
.context("there is no floating window")?;
.ok_or_else(|| anyhow!("there is no floating window"))?;
let mut container = Container::default();
container.add_window(window);
@@ -498,11 +607,11 @@ impl Workspace {
let container = self
.focused_container_mut()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.containers_mut().remove(focused_idx);
@@ -547,7 +656,7 @@ impl Workspace {
let container = self
.containers_mut()
.remove(focused_idx)
.context("there is not container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
// We don't remove any resize adjustments for a monocle, because when this container is
// inevitably reintegrated, it would be weird if it doesn't go back to the dimensions
@@ -559,7 +668,7 @@ impl Workspace {
self.monocle_container_mut()
.as_mut()
.context("there is no monocle container")?
.ok_or_else(|| anyhow!("there is no monocle container"))?
.load_focused_window();
Ok(())
@@ -568,12 +677,12 @@ impl Workspace {
pub fn reintegrate_monocle_container(&mut self) -> Result<()> {
let restore_idx = self
.monocle_container_restore_idx()
.context("there is no monocle restore index")?;
.ok_or_else(|| anyhow!("there is no monocle restore index"))?;
let container = self
.monocle_container_mut()
.as_ref()
.context("there is no monocle container")?;
.ok_or_else(|| anyhow!("there is no monocle container"))?;
let container = container.clone();
if restore_idx > self.containers().len() - 1 {
@@ -584,7 +693,7 @@ impl Workspace {
self.containers_mut().insert(restore_idx, container);
self.focus_container(restore_idx);
self.focused_container_mut()
.context("there is no container")?
.ok_or_else(|| anyhow!("there is no container"))?
.load_focused_window();
self.set_monocle_container(None);
@@ -598,11 +707,11 @@ impl Workspace {
let container = self
.focused_container_mut()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.containers_mut().remove(focused_idx);
@@ -626,12 +735,12 @@ impl Workspace {
pub fn reintegrate_maximized_window(&mut self) -> Result<()> {
let restore_idx = self
.maximized_window_restore_idx()
.context("there is no monocle restore index")?;
.ok_or_else(|| anyhow!("there is no monocle restore index"))?;
let window = self
.maximized_window()
.as_ref()
.context("there is no monocle container")?;
.ok_or_else(|| anyhow!("there is no monocle container"))?;
let window = *window;
if !self.containers().is_empty() && restore_idx > self.containers().len() - 1 {
@@ -646,7 +755,7 @@ impl Workspace {
self.focus_container(restore_idx);
self.focused_container_mut()
.context("there is no container")?
.ok_or_else(|| anyhow!("there is no container"))?
.load_focused_window();
self.set_maximized_window(None);

281
komorebic.lib.sample.ahk Normal file
View File

@@ -0,0 +1,281 @@
; Generated by komorebic.exe
Start(ffm) {
Run, komorebic.exe start --ffm %ffm%, , Hide
}
Stop() {
Run, komorebic.exe stop, , Hide
}
State() {
Run, komorebic.exe state, , Hide
}
Query(state_query) {
Run, komorebic.exe query %state_query%, , Hide
}
Subscribe(named_pipe) {
Run, komorebic.exe subscribe %named_pipe%, , Hide
}
Unsubscribe(named_pipe) {
Run, komorebic.exe unsubscribe %named_pipe%, , Hide
}
Log() {
Run, komorebic.exe log, , Hide
}
QuickSaveResize() {
Run, komorebic.exe quick-save-resize, , Hide
}
QuickLoadResize() {
Run, komorebic.exe quick-load-resize, , Hide
}
SaveResize(path) {
Run, komorebic.exe save-resize %path%, , Hide
}
LoadResize(path) {
Run, komorebic.exe load-resize %path%, , Hide
}
Focus(operation_direction) {
Run, komorebic.exe focus %operation_direction%, , Hide
}
Move(operation_direction) {
Run, komorebic.exe move %operation_direction%, , Hide
}
CycleFocus(cycle_direction) {
Run, komorebic.exe cycle-focus %cycle_direction%, , Hide
}
CycleMove(cycle_direction) {
Run, komorebic.exe cycle-move %cycle_direction%, , Hide
}
Stack(operation_direction) {
Run, komorebic.exe stack %operation_direction%, , Hide
}
Resize(edge, sizing) {
Run, komorebic.exe resize %edge% %sizing%, , Hide
}
ResizeAxis(axis, sizing) {
Run, komorebic.exe resize-axis %axis% %sizing%, , Hide
}
Unstack() {
Run, komorebic.exe unstack, , Hide
}
CycleStack(cycle_direction) {
Run, komorebic.exe cycle-stack %cycle_direction%, , Hide
}
MoveToMonitor(target) {
Run, komorebic.exe move-to-monitor %target%, , Hide
}
MoveToWorkspace(target) {
Run, komorebic.exe move-to-workspace %target%, , Hide
}
SendToMonitor(target) {
Run, komorebic.exe send-to-monitor %target%, , Hide
}
SendToWorkspace(target) {
Run, komorebic.exe send-to-workspace %target%, , Hide
}
FocusMonitor(target) {
Run, komorebic.exe focus-monitor %target%, , Hide
}
FocusWorkspace(target) {
Run, komorebic.exe focus-workspace %target%, , Hide
}
FocusMonitorWorkspace(target_monitor, target_workspace) {
Run, komorebic.exe focus-monitor-workspace %target_monitor% %target_workspace%, , Hide
}
CycleMonitor(cycle_direction) {
Run, komorebic.exe cycle-monitor %cycle_direction%, , Hide
}
CycleWorkspace(cycle_direction) {
Run, komorebic.exe cycle-workspace %cycle_direction%, , Hide
}
MoveWorkspaceToMonitor(target) {
Run, komorebic.exe move-workspace-to-monitor %target%, , Hide
}
NewWorkspace() {
Run, komorebic.exe new-workspace, , Hide
}
ResizeDelta(pixels) {
Run, komorebic.exe resize-delta %pixels%, , Hide
}
InvisibleBorders(left, top, right, bottom) {
Run, komorebic.exe invisible-borders %left% %top% %right% %bottom%, , Hide
}
WorkAreaOffset(left, top, right, bottom) {
Run, komorebic.exe work-area-offset %left% %top% %right% %bottom%, , Hide
}
AdjustContainerPadding(sizing, adjustment) {
Run, komorebic.exe adjust-container-padding %sizing% %adjustment%, , Hide
}
AdjustWorkspacePadding(sizing, adjustment) {
Run, komorebic.exe adjust-workspace-padding %sizing% %adjustment%, , Hide
}
ChangeLayout(default_layout) {
Run, komorebic.exe change-layout %default_layout%, , Hide
}
LoadCustomLayout(path) {
Run, komorebic.exe load-custom-layout %path%, , Hide
}
FlipLayout(axis) {
Run, komorebic.exe flip-layout %axis%, , Hide
}
Promote() {
Run, komorebic.exe promote, , Hide
}
Retile() {
Run, komorebic.exe retile, , Hide
}
EnsureWorkspaces(monitor, workspace_count) {
Run, komorebic.exe ensure-workspaces %monitor% %workspace_count%, , Hide
}
ContainerPadding(monitor, workspace, size) {
Run, komorebic.exe container-padding %monitor% %workspace% %size%, , Hide
}
WorkspacePadding(monitor, workspace, size) {
Run, komorebic.exe workspace-padding %monitor% %workspace% %size%, , Hide
}
WorkspaceLayout(monitor, workspace, value) {
Run, komorebic.exe workspace-layout %monitor% %workspace% %value%, , Hide
}
WorkspaceCustomLayout(monitor, workspace, path) {
Run, komorebic.exe workspace-custom-layout %monitor% %workspace% %path%, , Hide
}
WorkspaceTiling(monitor, workspace, value) {
Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide
}
WorkspaceName(monitor, workspace, value) {
Run, komorebic.exe workspace-name %monitor% %workspace% %value%, , Hide
}
ToggleWindowContainerBehaviour() {
Run, komorebic.exe toggle-window-container-behaviour, , Hide
}
TogglePause() {
Run, komorebic.exe toggle-pause, , Hide
}
ToggleTiling() {
Run, komorebic.exe toggle-tiling, , Hide
}
ToggleFloat() {
Run, komorebic.exe toggle-float, , Hide
}
ToggleMonocle() {
Run, komorebic.exe toggle-monocle, , Hide
}
ToggleMaximize() {
Run, komorebic.exe toggle-maximize, , Hide
}
RestoreWindows() {
Run, komorebic.exe restore-windows, , Hide
}
Manage() {
Run, komorebic.exe manage, , Hide
}
Unmanage() {
Run, komorebic.exe unmanage, , Hide
}
ReloadConfiguration() {
Run, komorebic.exe reload-configuration, , Hide
}
WatchConfiguration(boolean_state) {
Run, komorebic.exe watch-configuration %boolean_state%, , Hide
}
WindowHidingBehaviour(hiding_behaviour) {
Run, komorebic.exe window-hiding-behaviour %hiding_behaviour%, , Hide
}
FloatRule(identifier, id) {
Run, komorebic.exe float-rule %identifier% %id%, , Hide
}
ManageRule(identifier, id) {
Run, komorebic.exe manage-rule %identifier% %id%, , Hide
}
WorkspaceRule(identifier, id, monitor, workspace) {
Run, komorebic.exe workspace-rule %identifier% %id% %monitor% %workspace%, , Hide
}
IdentifyTrayApplication(identifier, id) {
Run, komorebic.exe identify-tray-application %identifier% %id%, , Hide
}
IdentifyBorderOverflow(identifier, id) {
Run, komorebic.exe identify-border-overflow %identifier% %id%, , Hide
}
FocusFollowsMouse(boolean_state, implementation) {
Run, komorebic.exe focus-follows-mouse %boolean_state% --implementation %implementation%, , Hide
}
ToggleFocusFollowsMouse(implementation) {
Run, komorebic.exe toggle-focus-follows-mouse --implementation %implementation%, , Hide
}
MouseFollowsFocus(boolean_state) {
Run, komorebic.exe mouse-follows-focus %boolean_state%, , Hide
}
ToggleMouseFollowsFocus() {
Run, komorebic.exe toggle-mouse-follows-focus, , Hide
}
AhkLibrary() {
Run, komorebic.exe ahk-library, , Hide
}

View File

@@ -1,25 +1,33 @@
[package]
name = "komorebic"
version = "0.1.2"
version = "0.1.7"
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"]
repository = "https://github.com/LGUG2Z/komorebi"
license = "MIT"
edition = "2018"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bindings = { package = "bindings", path = "../bindings" }
derive-ahk = { path = "../derive-ahk" }
komorebi-core = { path = "../komorebi-core" }
clap = "3.0.0-beta.4"
clap = { version = "3", features = ["derive", "wrap_help"] }
color-eyre = "0.5"
dirs = "3"
dirs = "4"
fs-tail = "0.1"
heck = "0.4"
paste = "1"
powershell_script = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uds_windows = "1"
[dependencies.windows]
version = "0.30"
features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging"
]

View File

@@ -2,6 +2,7 @@
#![allow(clippy::missing_errors_doc)]
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::ErrorKind;
@@ -11,27 +12,42 @@ use std::process::Command;
use clap::AppSettings;
use clap::ArgEnum;
use clap::Clap;
use color_eyre::eyre::ContextCompat;
use clap::Parser;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use fs_tail::TailedFile;
use heck::ToKebabCase;
use paste::paste;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use derive_ahk::AhkFunction;
use derive_ahk::AhkLibrary;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
use komorebi_core::CycleDirection;
use komorebi_core::Flip;
use komorebi_core::Layout;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;
#[derive(ArgEnum)]
trait AhkLibrary {
fn generate_ahk_library() -> String;
}
trait AhkFunction {
fn generate_ahk_function() -> String;
}
#[derive(Copy, Clone, ArgEnum)]
enum BooleanState {
Enable,
Disable,
@@ -48,10 +64,10 @@ impl From<BooleanState> for bool {
macro_rules! gen_enum_subcommand_args {
// SubCommand Pattern: Enum Type
( $( $name:ident: $element:ty ),+ ) => {
( $( $name:ident: $element:ty ),+ $(,)? ) => {
$(
paste! {
#[derive(clap::Clap)]
#[derive(clap::Parser, derive_ahk::AhkFunction)]
pub struct $name {
#[clap(arg_enum)]
[<$element:snake>]: $element
@@ -64,19 +80,25 @@ macro_rules! gen_enum_subcommand_args {
gen_enum_subcommand_args! {
Focus: OperationDirection,
Move: OperationDirection,
CycleFocus: CycleDirection,
CycleMove: CycleDirection,
CycleMonitor: CycleDirection,
CycleWorkspace: CycleDirection,
Stack: OperationDirection,
CycleStack: CycleDirection,
FlipLayout: Flip,
SetLayout: Layout,
FlipLayout: Axis,
ChangeLayout: DefaultLayout,
WatchConfiguration: BooleanState,
FocusFollowsMouse: BooleanState
MouseFollowsFocus: BooleanState,
Query: StateQuery,
WindowHidingBehaviour: HidingBehaviour,
}
macro_rules! gen_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Clap)]
#[derive(clap::Parser, derive_ahk::AhkFunction)]
pub struct $name {
/// Target index (zero-indexed)
target: usize,
@@ -88,8 +110,11 @@ macro_rules! gen_target_subcommand_args {
gen_target_subcommand_args! {
MoveToMonitor,
MoveToWorkspace,
SendToMonitor,
SendToWorkspace,
FocusMonitor,
FocusWorkspace
FocusWorkspace,
MoveWorkspaceToMonitor,
}
// Thanks to @danielhenrymantilla for showing me how to use cfg_attr with an optional argument like
@@ -97,10 +122,10 @@ gen_target_subcommand_args! {
macro_rules! gen_workspace_subcommand_args {
// Workspace Property: #[enum] Value Enum (if the value is an Enum)
// Workspace Property: Value Type (if the value is anything else)
( $( $name:ident: $(#[enum] $(@$arg_enum:tt)?)? $value:ty ),+ ) => (
( $( $name:ident: $(#[enum] $(@$arg_enum:tt)?)? $value:ty ),+ $(,)? ) => (
paste! {
$(
#[derive(clap::Clap)]
#[derive(clap::Parser, derive_ahk::AhkFunction)]
pub struct [<Workspace $name>] {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -111,7 +136,7 @@ macro_rules! gen_workspace_subcommand_args {
$(#[clap(arg_enum)] $($arg_enum)?)?
#[cfg_attr(
all($(FALSE $($arg_enum)?)?),
doc = ""$name" of the workspace as a "$value""
doc = ""$name " of the workspace as a "$value ""
)]
value: $value,
}
@@ -122,11 +147,23 @@ macro_rules! gen_workspace_subcommand_args {
gen_workspace_subcommand_args! {
Name: String,
Layout: #[enum] Layout,
Tiling: #[enum] BooleanState
Layout: #[enum] DefaultLayout,
Tiling: #[enum] BooleanState,
}
#[derive(Clap)]
#[derive(Parser, AhkFunction)]
pub struct WorkspaceCustomLayout {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
/// JSON or YAML file from which the custom layout definition should be loaded
path: String,
}
#[derive(Parser, AhkFunction)]
struct Resize {
#[clap(arg_enum)]
edge: OperationDirection,
@@ -134,7 +171,45 @@ struct Resize {
sizing: Sizing,
}
#[derive(Clap)]
#[derive(Parser, AhkFunction)]
struct ResizeAxis {
#[clap(arg_enum)]
axis: Axis,
#[clap(arg_enum)]
sizing: Sizing,
}
#[derive(Parser, AhkFunction)]
struct ResizeDelta {
/// The delta of pixels by which to increase or decrease window dimensions when resizing
pixels: i32,
}
#[derive(Parser, AhkFunction)]
struct InvisibleBorders {
/// Size of the left invisible border
left: i32,
/// Size of the top invisible border (usually 0)
top: i32,
/// Size of the right invisible border (usually left * 2)
right: i32,
/// Size of the bottom invisible border (usually the same as left)
bottom: i32,
}
#[derive(Parser, AhkFunction)]
struct WorkAreaOffset {
/// Size of the left work area offset (set right to left * 2 to maintain right padding)
left: i32,
/// Size of the top work area offset (set bottom to the same value to maintain bottom padding)
top: i32,
/// Size of the right work area offset
right: i32,
/// Size of the bottom work area offset
bottom: i32,
}
#[derive(Parser, AhkFunction)]
struct EnsureWorkspaces {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -142,33 +217,79 @@ struct EnsureWorkspaces {
workspace_count: usize,
}
#[derive(Clap)]
struct Padding {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
/// Pixels to pad with as an integer
size: i32,
#[derive(Parser, AhkFunction)]
struct FocusMonitorWorkspace {
/// Target monitor index (zero-indexed)
target_monitor: usize,
/// Workspace index on the target monitor (zero-indexed)
target_workspace: usize,
}
#[derive(Clap)]
struct PaddingAdjustment {
#[clap(arg_enum)]
sizing: Sizing,
/// Pixels to adjust by as an integer
adjustment: i32,
macro_rules! gen_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
pub struct $name {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
/// Pixels to pad with as an integer
size: i32,
}
)+
};
}
#[derive(Clap)]
struct ApplicationTarget {
#[clap(arg_enum)]
identifier: ApplicationIdentifier,
/// Identifier as a string
id: String,
gen_padding_subcommand_args! {
ContainerPadding,
WorkspacePadding,
}
#[derive(Clap)]
macro_rules! gen_padding_adjustment_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
pub struct $name {
#[clap(arg_enum)]
sizing: Sizing,
/// Pixels to adjust by as an integer
adjustment: i32,
}
)+
};
}
gen_padding_adjustment_subcommand_args! {
AdjustContainerPadding,
AdjustWorkspacePadding,
}
macro_rules! gen_application_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Parser, derive_ahk::AhkFunction)]
pub struct $name {
#[clap(arg_enum)]
identifier: ApplicationIdentifier,
/// Identifier as a string
id: String,
}
)+
};
}
gen_application_target_subcommand_args! {
FloatRule,
ManageRule,
IdentifyTrayApplication,
IdentifyBorderOverflow,
}
#[derive(Parser, AhkFunction)]
struct WorkspaceRule {
#[clap(arg_enum)]
identifier: ApplicationIdentifier,
@@ -180,35 +301,119 @@ struct WorkspaceRule {
workspace: usize,
}
#[derive(Clap)]
#[derive(Parser, AhkFunction)]
struct ToggleFocusFollowsMouse {
#[clap(arg_enum, short, long, default_value = "windows")]
implementation: FocusFollowsMouseImplementation,
}
#[derive(Parser, AhkFunction)]
struct FocusFollowsMouse {
#[clap(arg_enum, short, long, default_value = "windows")]
implementation: FocusFollowsMouseImplementation,
#[clap(arg_enum)]
boolean_state: BooleanState,
}
#[derive(Parser, AhkFunction)]
struct Start {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(long)]
ffm: bool,
}
#[derive(Parser, AhkFunction)]
struct SaveResize {
/// File to which the resize layout dimensions should be saved
path: String,
}
#[derive(Parser, AhkFunction)]
struct LoadResize {
/// File from which the resize layout dimensions should be loaded
path: String,
}
#[derive(Parser, AhkFunction)]
struct LoadCustomLayout {
/// JSON or YAML file from which the custom layout definition should be loaded
path: String,
}
#[derive(Parser, AhkFunction)]
struct Subscribe {
/// Name of the pipe to send event notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}
#[derive(Parser, AhkFunction)]
struct Unsubscribe {
/// Name of the pipe to stop sending event notifications to (without "\\.\pipe\" prepended)
named_pipe: String,
}
#[derive(Parser)]
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
struct Opts {
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Clap)]
#[derive(Parser, AhkLibrary)]
enum SubCommand {
/// Start komorebi.exe as a background process
Start,
Start(Start),
/// Stop the komorebi.exe process and restore all hidden windows
Stop,
/// Show a JSON representation of the current window manager state
State,
/// Query the current window manager state
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Query(Query),
/// Subscribe to komorebi events
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Subscribe(Subscribe),
/// Unsubscribe from komorebi events
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Unsubscribe(Unsubscribe),
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
Log,
/// Quicksave the current resize layout dimensions
#[clap(alias = "quick-save")]
QuickSaveResize,
/// Load the last quicksaved resize layout dimensions
#[clap(alias = "quick-load")]
QuickLoadResize,
/// Save the current resize layout dimensions to a file
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(alias = "save")]
SaveResize(SaveResize),
/// Load the resize layout dimensions from a file
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(alias = "load")]
LoadResize(LoadResize),
/// Change focus to the window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Focus(Focus),
/// Move the focused window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Move(Move),
/// Change focus to the window in the specified cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
CycleFocus(CycleFocus),
/// Move the focused window in the specified cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
CycleMove(CycleMove),
/// Stack the focused window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Stack(Stack),
/// Resize the focused window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Resize(Resize),
#[clap(alias = "resize")]
ResizeEdge(Resize),
/// Resize the focused window or primary column along the specified axis
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ResizeAxis(ResizeAxis),
/// Unstack the focused window
Unstack,
/// Cycle the focused stack in the specified cycle direction
@@ -220,23 +425,55 @@ enum SubCommand {
/// Move the focused window to the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
MoveToWorkspace(MoveToWorkspace),
/// Send the focused window to the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
SendToMonitor(SendToMonitor),
/// Send the focused window to the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
SendToWorkspace(SendToWorkspace),
/// Focus the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FocusMonitor(FocusMonitor),
/// Focus the specified workspace on the focused monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FocusWorkspace(FocusWorkspace),
/// Focus the specified workspace on the target monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FocusMonitorWorkspace(FocusMonitorWorkspace),
/// Focus the monitor in the given cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
CycleMonitor(CycleMonitor),
/// Focus the workspace in the given cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
CycleWorkspace(CycleWorkspace),
/// Move the focused workspace to the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
MoveWorkspaceToMonitor(MoveWorkspaceToMonitor),
/// Create and append a new workspace on the focused monitor
NewWorkspace,
/// Set the resize delta (used by resize-edge and resize-axis)
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ResizeDelta(ResizeDelta),
/// Set the invisible border dimensions around each window
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
InvisibleBorders(InvisibleBorders),
/// Set offsets to exclude parts of the work area from tiling
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkAreaOffset(WorkAreaOffset),
/// Adjust container padding on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
AdjustContainerPadding(PaddingAdjustment),
AdjustContainerPadding(AdjustContainerPadding),
/// Adjust workspace padding on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
AdjustWorkspacePadding(PaddingAdjustment),
AdjustWorkspacePadding(AdjustWorkspacePadding),
/// Set the layout on the focused workspace
ChangeLayout(SetLayout),
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ChangeLayout(ChangeLayout),
/// Load a custom layout from file for the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
LoadCustomLayout(LoadCustomLayout),
/// Flip the layout on the focused workspace (BSP only)
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FlipLayout(FlipLayout),
/// Promote the focused window to the top of the tree
Promote,
@@ -247,20 +484,25 @@ enum SubCommand {
EnsureWorkspaces(EnsureWorkspaces),
/// Set the container padding for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ContainerPadding(Padding),
ContainerPadding(ContainerPadding),
/// Set the workspace padding for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspacePadding(Padding),
WorkspacePadding(WorkspacePadding),
/// Set the layout for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspaceLayout(WorkspaceLayout),
/// Set a custom layout for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspaceCustomLayout(WorkspaceCustomLayout),
/// Enable or disable window tiling for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspaceTiling(WorkspaceTiling),
/// Set the workspace name for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspaceName(WorkspaceName),
/// Toggle the window manager on and off across all monitors
/// Toggle the behaviour for new windows (stacking or dynamic tiling)
ToggleWindowContainerBehaviour,
/// Toggle window tiling on the focused workspace
TogglePause,
/// Toggle window tiling on the focused workspace
ToggleTiling,
@@ -278,27 +520,44 @@ enum SubCommand {
Unmanage,
/// Reload ~/komorebi.ahk (if it exists)
ReloadConfiguration,
/// Toggle the automatic reloading of ~/komorebi.ahk (if it exists)
/// Enable or disable watching of ~/komorebi.ahk (if it exists)
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WatchConfiguration(WatchConfiguration),
/// Set the window behaviour when switching workspaces / cycling stacks
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WindowHidingBehaviour(WindowHidingBehaviour),
/// Add a rule to always float the specified application
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FloatRule(ApplicationTarget),
FloatRule(FloatRule),
/// Add a rule to always manage the specified application
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ManageRule(ApplicationTarget),
ManageRule(ManageRule),
/// Add a rule to associate an application with a workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspaceRule(WorkspaceRule),
/// Identify an application that closes to the system tray
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
IdentifyTrayApplication(ApplicationTarget),
IdentifyTrayApplication(IdentifyTrayApplication),
/// Identify an application that has overflowing borders
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
IdentifyBorderOverflow(IdentifyBorderOverflow),
/// Enable or disable focus follows mouse for the operating system
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FocusFollowsMouse(FocusFollowsMouse),
/// Toggle focus follows mouse for the operating system
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ToggleFocusFollowsMouse(ToggleFocusFollowsMouse),
/// Enable or disable mouse follows focus on all workspaces
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
MouseFollowsFocus(MouseFollowsFocus),
/// Toggle mouse follows focus on all workspaces
ToggleMouseFollowsFocus,
/// Generate a library of AutoHotKey helper functions
AhkLibrary,
}
pub fn send_message(bytes: &[u8]) -> Result<()> {
let mut socket = dirs::home_dir().context("there is no home directory")?;
let mut socket = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
socket.push("komorebi.sock");
let socket = socket.as_path();
@@ -311,6 +570,31 @@ fn main() -> Result<()> {
let opts: Opts = Opts::parse();
match opts.subcmd {
SubCommand::AhkLibrary => {
let mut library =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
library.push("komorebic.lib.ahk");
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(library.clone())?;
file.write_all(SubCommand::generate_ahk_library().as_bytes())?;
println!(
"\nAHK helper library for komorebic written to {}",
library.to_str().ok_or_else(|| anyhow!(
"could not find the path to the generated ahk lib file"
))?
);
println!(
"\nYou can include the library at the top of your ~/komorebi.ahk config with this line:"
);
println!("\n#Include %A_ScriptDir%\\komorebic.lib.ahk");
}
SubCommand::Log => {
let mut color_log = std::env::temp_dir();
color_log.push("komorebi.log");
@@ -335,12 +619,49 @@ fn main() -> Result<()> {
SubCommand::Move(arg) => {
send_message(&*SocketMessage::MoveWindow(arg.operation_direction).as_bytes()?)?;
}
SubCommand::CycleFocus(arg) => {
send_message(&*SocketMessage::CycleFocusWindow(arg.cycle_direction).as_bytes()?)?;
}
SubCommand::CycleMove(arg) => {
send_message(&*SocketMessage::CycleMoveWindow(arg.cycle_direction).as_bytes()?)?;
}
SubCommand::MoveToMonitor(arg) => {
send_message(&*SocketMessage::MoveContainerToMonitorNumber(arg.target).as_bytes()?)?;
}
SubCommand::MoveToWorkspace(arg) => {
send_message(&*SocketMessage::MoveContainerToWorkspaceNumber(arg.target).as_bytes()?)?;
}
SubCommand::SendToMonitor(arg) => {
send_message(&*SocketMessage::SendContainerToMonitorNumber(arg.target).as_bytes()?)?;
}
SubCommand::SendToWorkspace(arg) => {
send_message(&*SocketMessage::SendContainerToWorkspaceNumber(arg.target).as_bytes()?)?;
}
SubCommand::MoveWorkspaceToMonitor(arg) => {
send_message(&*SocketMessage::MoveWorkspaceToMonitorNumber(arg.target).as_bytes()?)?;
}
SubCommand::InvisibleBorders(arg) => {
send_message(
&*SocketMessage::InvisibleBorders(Rect {
left: arg.left,
top: arg.top,
right: arg.right,
bottom: arg.bottom,
})
.as_bytes()?,
)?;
}
SubCommand::WorkAreaOffset(arg) => {
send_message(
&*SocketMessage::WorkAreaOffset(Rect {
left: arg.left,
top: arg.top,
right: arg.right,
bottom: arg.bottom,
})
.as_bytes()?,
)?;
}
SubCommand::ContainerPadding(arg) => {
send_message(
&*SocketMessage::ContainerPadding(arg.monitor, arg.workspace, arg.size)
@@ -363,6 +684,9 @@ fn main() -> Result<()> {
&*SocketMessage::AdjustContainerPadding(arg.sizing, arg.adjustment).as_bytes()?,
)?;
}
SubCommand::ToggleFocusFollowsMouse(arg) => {
send_message(&*SocketMessage::ToggleFocusFollowsMouse(arg.implementation).as_bytes()?)?;
}
SubCommand::ToggleTiling => {
send_message(&*SocketMessage::ToggleTiling.as_bytes()?)?;
}
@@ -381,13 +705,23 @@ fn main() -> Result<()> {
.as_bytes()?,
)?;
}
SubCommand::WorkspaceCustomLayout(arg) => {
send_message(
&*SocketMessage::WorkspaceLayoutCustom(
arg.monitor,
arg.workspace,
resolve_windows_path(&arg.path)?,
)
.as_bytes()?,
)?;
}
SubCommand::WorkspaceTiling(arg) => {
send_message(
&*SocketMessage::WorkspaceTiling(arg.monitor, arg.workspace, arg.value.into())
.as_bytes()?,
)?;
}
SubCommand::Start => {
SubCommand::Start(arg) => {
let mut buf: PathBuf;
// The komorebi.ps1 shim will only exist in the Path if installed by Scoop
@@ -400,21 +734,36 @@ fn main() -> Result<()> {
buf.pop(); // %USERPROFILE%\scoop\shims
buf.pop(); // %USERPROFILE%\scoop
buf.push("apps\\komorebi\\current\\komorebi.exe"); //%USERPROFILE%\scoop\komorebi\current\komorebi.exe
Option::from(
buf.to_str()
.context("cannot create a string from the scoop komorebi path")?,
)
Option::from(buf.to_str().ok_or_else(|| {
anyhow!("cannot create a string from the scoop komorebi path")
})?)
}
}
} else {
None
};
let script = if let Some(exec) = exec {
format!("Start-Process '{}' -WindowStyle hidden", exec)
} else {
String::from("Start-Process komorebi -WindowStyle hidden")
};
let script = exec.map_or_else(
|| {
if arg.ffm {
String::from(
"Start-Process komorebi.exe -ArgumentList '--ffm' -WindowStyle hidden",
)
} else {
String::from("Start-Process komorebi.exe -WindowStyle hidden")
}
},
|exec| {
if arg.ffm {
format!(
"Start-Process '{}' -ArgumentList '--ffm' -WindowStyle hidden",
exec
)
} else {
format!("Start-Process '{}' -WindowStyle hidden", exec)
}
},
);
match powershell_script::run(&script, true) {
Ok(output) => {
@@ -450,10 +799,15 @@ fn main() -> Result<()> {
send_message(&*SocketMessage::CycleStack(arg.cycle_direction).as_bytes()?)?;
}
SubCommand::ChangeLayout(arg) => {
send_message(&*SocketMessage::ChangeLayout(arg.layout).as_bytes()?)?;
send_message(&*SocketMessage::ChangeLayout(arg.default_layout).as_bytes()?)?;
}
SubCommand::LoadCustomLayout(arg) => {
send_message(
&*SocketMessage::ChangeLayoutCustom(resolve_windows_path(&arg.path)?).as_bytes()?,
)?;
}
SubCommand::FlipLayout(arg) => {
send_message(&*SocketMessage::FlipLayout(arg.flip).as_bytes()?)?;
send_message(&*SocketMessage::FlipLayout(arg.axis).as_bytes()?)?;
}
SubCommand::FocusMonitor(arg) => {
send_message(&*SocketMessage::FocusMonitorNumber(arg.target).as_bytes()?)?;
@@ -461,6 +815,21 @@ fn main() -> Result<()> {
SubCommand::FocusWorkspace(arg) => {
send_message(&*SocketMessage::FocusWorkspaceNumber(arg.target).as_bytes()?)?;
}
SubCommand::FocusMonitorWorkspace(arg) => {
send_message(
&*SocketMessage::FocusMonitorWorkspaceNumber(
arg.target_monitor,
arg.target_workspace,
)
.as_bytes()?,
)?;
}
SubCommand::CycleMonitor(arg) => {
send_message(&*SocketMessage::CycleFocusMonitor(arg.cycle_direction).as_bytes()?)?;
}
SubCommand::CycleWorkspace(arg) => {
send_message(&*SocketMessage::CycleFocusWorkspace(arg.cycle_direction).as_bytes()?)?;
}
SubCommand::NewWorkspace => {
send_message(&*SocketMessage::NewWorkspace.as_bytes()?)?;
}
@@ -477,7 +846,7 @@ fn main() -> Result<()> {
)?;
}
SubCommand::State => {
let home = dirs::home_dir().context("there is no home directory")?;
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut socket = home;
socket.push("komorebic.sock");
let socket = socket.as_path();
@@ -510,8 +879,43 @@ fn main() -> Result<()> {
}
}
}
SubCommand::Query(arg) => {
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut socket = home;
socket.push("komorebic.sock");
let socket = socket.as_path();
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());
}
},
};
send_message(&*SocketMessage::Query(arg.state_query).as_bytes()?)?;
let listener = UnixListener::bind(&socket)?;
match listener.accept() {
Ok(incoming) => {
let stream = BufReader::new(incoming.0);
for line in stream.lines() {
println!("{}", line?);
}
return Ok(());
}
Err(error) => {
panic!("{}", error);
}
}
}
SubCommand::RestoreWindows => {
let mut hwnd_json = dirs::home_dir().context("there is no home directory")?;
let mut hwnd_json =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
hwnd_json.push("komorebi.hwnd.json");
let file = File::open(hwnd_json)?;
@@ -522,26 +926,27 @@ fn main() -> Result<()> {
restore_window(HWND(hwnd));
}
}
SubCommand::Resize(resize) => {
send_message(&*SocketMessage::ResizeWindow(resize.edge, resize.sizing).as_bytes()?)?;
SubCommand::ResizeEdge(resize) => {
send_message(
&*SocketMessage::ResizeWindowEdge(resize.edge, resize.sizing).as_bytes()?,
)?;
}
SubCommand::ResizeAxis(arg) => {
send_message(&*SocketMessage::ResizeWindowAxis(arg.axis, arg.sizing).as_bytes()?)?;
}
SubCommand::FocusFollowsMouse(arg) => {
let enable = match arg.boolean_state {
BooleanState::Enable => true,
BooleanState::Disable => false,
};
send_message(&*SocketMessage::FocusFollowsMouse(enable).as_bytes()?)?;
send_message(
&*SocketMessage::FocusFollowsMouse(arg.implementation, arg.boolean_state.into())
.as_bytes()?,
)?;
}
SubCommand::ReloadConfiguration => {
send_message(&*SocketMessage::ReloadConfiguration.as_bytes()?)?;
}
SubCommand::WatchConfiguration(arg) => {
let enable = match arg.boolean_state {
BooleanState::Enable => true,
BooleanState::Disable => false,
};
send_message(&*SocketMessage::WatchConfiguration(enable).as_bytes()?)?;
send_message(
&*SocketMessage::WatchConfiguration(arg.boolean_state.into()).as_bytes()?,
)?;
}
SubCommand::IdentifyTrayApplication(target) => {
send_message(
@@ -549,17 +954,86 @@ fn main() -> Result<()> {
.as_bytes()?,
)?;
}
SubCommand::IdentifyBorderOverflow(target) => {
send_message(
&*SocketMessage::IdentifyBorderOverflow(target.identifier, target.id).as_bytes()?,
)?;
}
SubCommand::Manage => {
send_message(&*SocketMessage::ManageFocusedWindow.as_bytes()?)?;
}
SubCommand::Unmanage => {
send_message(&*SocketMessage::UnmanageFocusedWindow.as_bytes()?)?;
}
SubCommand::QuickSaveResize => {
send_message(&*SocketMessage::QuickSave.as_bytes()?)?;
}
SubCommand::QuickLoadResize => {
send_message(&*SocketMessage::QuickLoad.as_bytes()?)?;
}
SubCommand::SaveResize(arg) => {
send_message(&*SocketMessage::Save(resolve_windows_path(&arg.path)?).as_bytes()?)?;
}
SubCommand::LoadResize(arg) => {
send_message(&*SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?;
}
SubCommand::Subscribe(arg) => {
send_message(&*SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
}
SubCommand::Unsubscribe(arg) => {
send_message(&*SocketMessage::RemoveSubscriber(arg.named_pipe).as_bytes()?)?;
}
SubCommand::ToggleMouseFollowsFocus => {
send_message(&*SocketMessage::ToggleMouseFollowsFocus.as_bytes()?)?;
}
SubCommand::MouseFollowsFocus(arg) => {
send_message(&*SocketMessage::MouseFollowsFocus(arg.boolean_state.into()).as_bytes()?)?;
}
SubCommand::ResizeDelta(arg) => {
send_message(&*SocketMessage::ResizeDelta(arg.pixels).as_bytes()?)?;
}
SubCommand::ToggleWindowContainerBehaviour => {
send_message(&*SocketMessage::ToggleWindowContainerBehaviour.as_bytes()?)?;
}
SubCommand::WindowHidingBehaviour(arg) => {
send_message(&*SocketMessage::WindowHidingBehaviour(arg.hiding_behaviour).as_bytes()?)?;
}
}
Ok(())
}
fn resolve_windows_path(raw_path: &str) -> Result<PathBuf> {
let path = if raw_path.starts_with('~') {
raw_path.replacen(
'~',
&dirs::home_dir()
.ok_or_else(|| anyhow!("there is no home directory"))?
.display()
.to_string(),
1,
)
} else {
raw_path.to_string()
};
let full_path = PathBuf::from(path);
let parent = full_path
.parent()
.ok_or_else(|| anyhow!("cannot parse directory"))?;
let file = full_path
.components()
.last()
.ok_or_else(|| anyhow!("cannot parse filename"))?;
let mut canonicalized = std::fs::canonicalize(parent)?;
canonicalized.push(file);
Ok(canonicalized)
}
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
// BOOL is returned but does not signify whether or not the operation was succesful
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow

1
rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
imports_granularity = "Item"