Compare commits

...

54 Commits

Author SHA1 Message Date
LGUG2Z
506600d689 feat(bar): don't think i'll pursue this 2022-04-25 08:04:18 -07:00
LGUG2Z
a10b13c799 fix(windows): ensure result processor is type-agnostic 2022-04-21 13:59:14 -07:00
LGUG2Z
1e69c65c25 fix(wm): ignore polling updates from com hwnds
This commit ensures that what monitor reconciliation is triggered from a
MonitorPoll event, the focused monitor is only updated when the HWND
associated with the event is known not to be tied to a specific (in this
case, the primary) monitor.

This ensures that silent state updates do not occur and avoids
unexpected behaviour when performing operations relative to the
currently focused window on a non-primary display (focus, move etc.)
2022-04-20 16:44:19 -07:00
LGUG2Z
711ab8d59b feat(wm): add cmd for unmanaged hwnd op behaviour
This commit adds a new command, 'unmanaged-window-operation-behaviour'
which allows the user to configure their desired behaviour in situations
when sending window container commands which operate on the focused
window container in the workspace state, but having an unmanaged window
as the foreground hwnd.

The default previously was previously Op (and this remains the default
with these new changes), but the user can now select NoOp, which will
return an error when the focused hwnd is unmanaged and not allow any
write operations to take place on the focused workspace state.

resolve #133
2022-04-19 17:13:27 -07:00
LGUG2Z
e1c36c9190 fix(wm): update origin ws after container removal
This commit ensures that the origin workspace will be updated after a
container is removed to be sent to a target workspace (specified, or
currently focused) on another monitor.

With this change in place, moving window containers to another monitor
should not result in a ghost container that remains until the next
retile on the origin workspace.

fix #132
2022-04-19 11:10:22 -07:00
LGUG2Z
686d013734 fix(windows): reintroduce hwnd val checks
@riverar pointed out on Discord that I had my if and else clauses here
mixed up. This commit reintroduces null value checks for HWNDs returned
from Windows API calls.
2022-04-15 17:31:57 -07:00
LGUG2Z
fad4cbf019 fix(ahk): quote app ids in generated code
Previously, generated AHK did not surround with quotes inputs which
could contain spaces such as application titles. This commit ensures
that in any generated AHK code where an application id is passed to a
komorebic.exe command as input, that input will always be quoted.

This fixes bugs related to float rules, manage rules and other
application identification commands not being properly executed via
komorebic called from the generated AHK files when the app id contained
a space.
2022-04-15 12:22:20 -07:00
LGUG2Z
839f8c9bf7 fix(windows): remove hwnd val checks on 0.35
Small commit to temporarily handle a regression introduced by my changes
when upgrading from 0.34 to 0.35.

Checking for a 0 HWND value results in an Err being propagated in fns
like GetForegroundWindow, while the error message just reads "The
operation completed successfully. (os error 0)".

This behaviour was causing regressions in features such as window
floating which seems to be resolved by removing the 0 HWND check.
2022-04-15 08:28:07 -07:00
LGUG2Z
5d468ae70a docs(readme): add cfgen explanation and demos 2022-04-14 17:21:09 -07:00
LGUG2Z
93edcfaa2f Merge branch 'master' into feature/config-generation 2022-04-13 19:40:37 -07:00
LGUG2Z
02a3220cbd chore(deps): bump windows-rs from 0.34 to 0.35 2022-04-13 19:39:26 -07:00
dependabot[bot]
4b6a7c05e0 chore(deps): bump actions/upload-artifact from 2 to 3 (#129)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-12 10:03:51 -07:00
LGUG2Z
304158cb1f feat(config): add cfgen override merging
This commit adds a second optional argument to the ahk-asc command which
can take an override yaml file. This file can include either entirely
new entries that are not suitable for the asc definitions in the
community repo, or overrides for entries that exist in the community asc
definitions files which will take precedence in the generated ahk file.

This can be useful for example, when the default behaviour for an app is
to minimise to system tray, but that option has been disabled on a
user's computer, making the 'tray_and_multi_window' option no longer
appropriate for their komorebi configuration.

In the case of wanting to override an existing entry, only the "name"
key needs to match; upon a match the entry from the community asc
definitions will be entirely replaced with the entry from the override
definitions.

re #62
2022-04-03 18:17:39 -07:00
LGUG2Z
c426c06c01 feat(config): add fmt cmd & float rule comments
This commit adds a fmt command which allows users to prepare PRs to the
configuration repository in a unified way.

The 'custom' formatter basically just ensures that the yaml array is
sorted by application name to make for easier diffs.

Serializing of Option::None has been disabled to keep the yaml file more
concise.

Finally, an option for adding comments to float rules has been included
as some of these rules can be quite esoteric and there is value in
having them annotated with comments in the configuration to preserve and
pass down the knowledge.

The config generation command has been renamed to
'ahk-app-specific-configuration' (with a short alias of 'ahk-asc') to
emphasise that an ahk file is being generated (similar to
'ahk-library').

re #62
2022-04-03 13:43:51 -07:00
Eric Reeves
c2cc21d09d float-rule Syntax Error Fixed (#127) 2022-04-02 08:45:57 -07:00
LGUG2Z
09a24b89e5 feat(config): add cfgen for apps based on yaml def
This commit introduces a configuration generator for
application-specific config options passed to the cli via a file path.

The hope is to have a public repository that any user can contribute
application-specific configs and fixes to, and for the generated AHK to
be available to any new user as part of the initial setup to make the
onboarding as frictionless as possible.

re #62
2022-04-01 18:25:05 -07:00
LGUG2Z
4686d5e346 feat(wm): add cmd to id layered apps
Users on Discord noted that Microsoft Office applications were not being
handled correctly by the wm. After some investigation it was clear that
this was because the application windows had WS_EX_LAYERED set.

This had only been seen once before with Steam, and so a whitelist for
layered applications was previously added to the codebase with steam.exe
hard-coded, but this had not been exposed via the cli.

This commit adds a command to allow users to specify layered
applications which should be managed, and also renames the whitelist to
reflect that classes can also be used to identify applications on the
whitelist.

A section has been added to the README to guide Microsoft Office users
to guide Microsoft Office users in configuring komorebi to correctly
handle Office applications with Word given as an example.

This commit also renames the identify-border-overflow command to
identify-border-overflow-application for consistency, while retaining
the previous command as an alias to maintain compatibility with existing
user configurations.

It should be noted however, that those like me who are using the
generated komorebic AHK library, will have to update any AHK function
calls to use IdentifyBorderOverflowApplication().

resolve #124
2022-03-30 11:04:41 -07:00
LGUG2Z
532adc9c6c docs(readme): fix heading for dynamic layouts section 2022-03-29 09:39:20 -07:00
LGUG2Z
a4e8286327 feat(wm): allow cycling for max & monacle windows
This commit introduces focus cycling behaviour for a workspace when
either a maximized window or a monocle window exists.

Now, the container in the cycle direction relative to the current window
container will take the maximized or monocle window container space
whenever the cycle-focus command is called.

resolve #97
2022-03-29 07:31:18 -07:00
LGUG2Z
75234caa98 feat(wm): add dynamic layout selection rules
This commit adds a new feature which allows the user to specify a set of
rules for a specific workspace that will be used to calculate which
layout to apply to that workspace at any given time.

The rule consists of a usize, which identifies the threshold of window
containers which need to be visible on the workspace to activate the
rule, and a layout, which will be applied to the workspace when the rule
is activated.

Both default and custom layouts can be used in workspace layout rules.

When a workspace has layout rules in effect, manually changing the
layout will not work again until the rules for that workspace have been
cleared.

This feature came about after trying but failing to modify the custom
layout code in such a way that the width percentage of a primary column
in a custom layout might be propagated to the fallback columnar layout
when the tertiary column threshold is not met.

Although this new feature introduces more complexity, it is strictly
opt-in and can be completely ignored if the user has no interest in
adjusting layouts based on the visible window count.

re #121
2022-03-28 14:49:16 -07:00
LGUG2Z
31b8be1481 feat(wm): add cmd to id apps with odd launch event
A user on the Discord noted that PyCharm windows were not being managed
as expected when initially launched. After some digging this seems to be
the same issue that was addressed for IntelliJ and Firefox early on in
development, where these applications send EVENT_OBJECT_NAMECHANGE on
launch instead of the regular event when drawing a new window.

The OBJECT_NAME_CHANGE_ON_LAUNCH vec was not previously exposed via
komorebic to allow users to identify other applications that exhibit the
same behaviour. This commit adds a command to allow users to specify
further applications in their configuration files.
2022-03-28 10:08:34 -07:00
dependabot[bot]
634bc04d76 chore(deps): bump actions/cache from 2 to 3 (#123)
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-21 08:46:35 -07:00
LGUG2Z
3b30c10ebb chore(deps): bump minor and patch versions with cargo update 2022-03-18 16:44:22 -07:00
LGUG2Z
3eade94032 chore(deps): bump windows-rs from 0.33 to 0.34 2022-03-18 16:43:31 -07:00
dependabot[bot]
e46f1f4f6d chore(deps): bump actions/checkout from 2 to 3 (#122)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-07 07:59:33 -08:00
dependabot[bot]
45ea630e6a chore(deps): bump powershell_script from 0.2.1 to 0.3.2 (#118)
Bumps [powershell_script](https://github.com/cfsamson/powershell-script) from 0.2.1 to 0.3.2.
- [Release notes](https://github.com/cfsamson/powershell-script/releases)
- [Commits](https://github.com/cfsamson/powershell-script/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: LGUG2Z <jadeiqbal@fastmail.com>
2022-03-03 17:24:52 -08:00
LGUG2Z
ed01bb674f refactor(clap): fix deprecations 2022-03-03 16:25:57 -08:00
dependabot[bot]
a9534fa49c chore(deps): bump clap from 3.0.14 to 3.1.3
Bumps [clap](https://github.com/clap-rs/clap) from 3.0.14 to 3.1.3.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v3.0.14...v3.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-03 16:25:57 -08:00
dependabot[bot]
d4c0c35f3a chore(deps): bump windows from 0.32.0 to 0.33.0
Bumps [windows](https://github.com/microsoft/windows-rs) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/microsoft/windows-rs/releases)
- [Commits](https://github.com/microsoft/windows-rs/compare/0.32.0...0.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-03 16:07:46 -08:00
dependabot[bot]
f6e0f5ab81 chore(deps): bump crossbeam-utils from 0.8.6 to 0.8.7
Bumps [crossbeam-utils](https://github.com/crossbeam-rs/crossbeam) from 0.8.6 to 0.8.7.
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-utils-0.8.6...crossbeam-utils-0.8.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 08:25:41 -08:00
dependabot[bot]
51139b9e0c chore(deps): bump color-eyre from 0.6.0 to 0.6.1
Bumps [color-eyre](https://github.com/yaahc/color-eyre) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/yaahc/color-eyre/releases)
- [Changelog](https://github.com/yaahc/color-eyre/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yaahc/color-eyre/compare/v0.6.0...v0.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 08:25:26 -08:00
dependabot[bot]
d7f1190152 chore(deps): bump strum from 0.23.0 to 0.24.0
Bumps [strum](https://github.com/Peternator7/strum) from 0.23.0 to 0.24.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>
2022-03-01 08:10:15 -08:00
dependabot[bot]
b62d77501a chore(deps): bump tracing-appender from 0.2.0 to 0.2.1
Bumps [tracing-appender](https://github.com/tokio-rs/tracing) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-appender-0.2.0...tracing-appender-0.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 08:09:43 -08:00
dependabot[bot]
7cb60ca7c5 chore(deps): bump sysinfo from 0.23.0 to 0.23.5
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.23.0 to 0.23.5.
- [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>
2022-03-01 08:09:28 -08:00
dependabot[bot]
43edf13bb2 chore(deps): bump serde_json from 1.0.78 to 1.0.79
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.78 to 1.0.79.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.78...v1.0.79)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 08:09:06 -08:00
LGUG2Z
cd894655db refactor(clippy): impl as_ref on wm struct 2022-02-05 13:59:05 -08:00
LGUG2Z
02c54734fb feat(wm): add send-to-monitor-workspace cmd
This commit adds a dedicated command to send a window container to a
specific workspace on a target monitor.
2022-02-05 13:42:16 -08:00
LGUG2Z
4a3f7ee34e chore(deps): bump windows-rs from 0.30 to 0.32 2022-02-03 14:21:07 -08:00
LGUG2Z
2db0d888c1 feat(subscriptions): add cmd to gen json schema
This commit introduces the 'notification-schema' command to generate a
JSON schema of the Notification struct which gets sent when notifying
subscribers of updates.
2022-02-01 12:38:11 -08:00
LGUG2Z
cf5a41b5eb chore(deps): cargo update 2022-02-01 10:13:50 -08:00
dependabot[bot]
e4ee298606 chore(deps): bump sysinfo from 0.22.5 to 0.23.0
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.22.5 to 0.23.0.
- [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-02-01 08:41:24 -08:00
dependabot[bot]
38c0b25a1c chore(deps): bump serde from 1.0.133 to 1.0.136
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.133 to 1.0.136.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.133...v1.0.136)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 08:10:36 -08:00
dependabot[bot]
d1b6a63af5 chore(deps): bump color-eyre from 0.5.11 to 0.6.0
Bumps [color-eyre](https://github.com/yaahc/color-eyre) from 0.5.11 to 0.6.0.
- [Release notes](https://github.com/yaahc/color-eyre/releases)
- [Changelog](https://github.com/yaahc/color-eyre/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yaahc/color-eyre/compare/v0.5.11...v0.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 08:10:23 -08:00
dependabot[bot]
c246b209c4 chore(deps): bump which from 4.2.2 to 4.2.4
Bumps [which](https://github.com/harryfei/which-rs) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/harryfei/which-rs/releases)
- [Commits](https://github.com/harryfei/which-rs/compare/4.2.2...4.2.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 08:06:39 -08:00
dependabot[bot]
a2e1b8c967 chore(deps): bump parking_lot from 0.11.2 to 0.12.0
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.11.2 to 0.12.0.
- [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.2...0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 07:59:09 -08:00
dependabot[bot]
cb387025d2 chore(deps): bump tracing-subscriber from 0.3.6 to 0.3.7
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.6 to 0.3.7.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.6...tracing-subscriber-0.3.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 07:58:13 -08:00
dependabot[bot]
6655d290f2 chore(deps): bump quote from 1.0.14 to 1.0.15
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.14...1.0.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 07:57:52 -08:00
dependabot[bot]
999f2ae2d4 chore(deps): bump clap from 3.0.8 to 3.0.13
Bumps [clap](https://github.com/clap-rs/clap) from 3.0.8 to 3.0.13.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v3.0.8...v3.0.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 07:57:39 -08:00
dependabot[bot]
cddc69d2bf chore(deps): bump serde_json from 1.0.75 to 1.0.78
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.75 to 1.0.78.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.75...v1.0.78)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-01 07:57:18 -08:00
LGUG2Z
43b2366378 feat(config): allow users to define config dir
This commit introduces a change to allow users to set a custom
configuration directory for Komorebi to address concerns about $HOME
getting cluttered.

The custom directory can be set with the environment variable
$Env:KOMOREBI_CONFIG_HOME (this should probably be done in $PROFILE).

If this variable is not set, komorebi will default to using
the $HOME directory.

resolve #61
2022-01-28 09:35:42 -08:00
LGUG2Z
e67425f841 docs(readme): update scoop install instructions 2022-01-28 08:35:44 -08:00
LGUG2Z
b2a34204c6 chore(release): v0.1.8 2022-01-27 11:10:29 -08:00
LGUG2Z
d18283969a fix(scoop): allow duplicate shim process
This commit addresses issues that users have been faced with when
installing komorebi with scoop, which resulted in komorebi exiting
almost immediately without providing any feedback as to what had
happened.

Scoop launches komorebi using an exe shim of the same name, which
results in two komorebi.exe named processes running at the same time.

This situation then fails the startup check which attempts to ensure
that only one instance of komorebi.exe ever runs at any given time.

The process startup check has been updated to allow for two komorebi.exe
named processes to be running if one of them is recognised as a Scoop
shim process.

fix #95
2022-01-27 11:07:23 -08:00
LGUG2Z
0138a313c0 docs(readme): update discord invite link 2022-01-17 09:12:12 -08:00
46 changed files with 4580 additions and 807 deletions

View File

@@ -28,7 +28,7 @@ jobs:
target:
- x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Prep cargo dirs
@@ -42,7 +42,7 @@ jobs:
echo "TARGET=${{ matrix.target }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
echo "SKIP_TESTS=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
- name: Cache cargo registry, git trees and binaries
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
@@ -55,7 +55,7 @@ jobs:
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
shell: bash
- name: Cache cargo build
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: target
key: ${{ github.base_ref }}-${{ github.head_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
@@ -77,7 +77,7 @@ jobs:
run: |
cargo build --locked --release --target ${{ matrix.target }}
- name: Upload the built artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: komorebi-${{ matrix.target }}
path: |

2510
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,5 +4,6 @@ members = [
"derive-ahk",
"komorebi",
"komorebi-core",
"komorebi-bar",
"komorebic"
]

290
README.md
View File

@@ -18,7 +18,7 @@ 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,
There is a [Discord server](https://discord.gg/mGkn66PHkx) 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.
@@ -27,6 +27,23 @@ Articles, blog posts, demos, and videos about _komorebi_ can be added to this li
- [Moving to Windows from Linux Pt 1](https://kvwu.io/posts/moving-to-windows/)
- [Windows 下的现代化平铺窗口管理器 komorebi](https://zhuanlan.zhihu.com/p/455064481)
## Demonstrations
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
11 with a terminal emulator, a web browser and a code editor. The original
video can be viewed
[here](https://twitter.com/haxibami/status/1501560766578659332).
https://user-images.githubusercontent.com/13164844/163496447-20c3ff0a-c5d8-40d1-9cc8-156c4cebf12e.mp4
[@aik2mlj](https://github.com/aik2mlj) showing _komorebi_ running on Windows 11
with multiple workspaces, terminal emulators, a web browser, and the
[yasb](https://github.com/DenBot/yasb) status bar with the _komorebi_ workspace
widget enabled. The original video can be viewed
[here](https://zhuanlan.zhihu.com/p/455064481).
https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-96fb-a8985380bc70.mp4
## Description
_komorebi_ only responds to [WinEvents](https://docs.microsoft.com/en-us/windows/win32/winauto/event-constants) and the
@@ -88,14 +105,16 @@ PowerShell prompt), and then move the binaries to that directory.
If you use the [Scoop](https://scoop.sh/) command line installer, you can run the following commands to install the
binaries from the latest GitHub Release:
```
scoop bucket add komorebi https://github.com/LGUG2Z/komorebi-bucket
```powershell
scoop bucket add extras
scoop install komorebi
```
If you install _komorebi_ using Scoop, the binaries will automatically be added to your `Path` and a command will be
shown for you to run in order to get started using the sample configuration file.
Thanks to [@sitiom](https://github.com/sitiom) for getting _komorebi_ added to the popular Scoop Extras bucket.
### Building from Source
If you prefer to compile _komorebi_ from source, you will need
@@ -138,6 +157,68 @@ for _komorebi_ can be found [here](https://gist.github.com/crosstyan/dafacc0778d
### Common First-Time Tips
#### Generating Common Application-Specific Configurations
A curated selection of application-specific configurations can be generated to
help ease the setup for first-time users.
[`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
contains YAML definitions of settings that are known to make tricky
applications behave as expected. These YAML definitions can be used to generate
an AHK file which you can import at the start of your own `komorebi.ahk` file,
leaving you to focus primarily on your desired keybindings and workspace
configurations.
If you have settings for an application that you think should be part of this
curated selection, please open a PR on the configuration repository.
In the event that your PR is not accepted, or if you find there are any
settings that you wish to override, this can easily be done using an override
file.
```powershell
# Clone and enter the repository
git clone https://github.com/LGUG2Z/komorebi-application-specific-configuration.git
cd komorebi-application-specific-configuration
# Use komorebic to generate an AHK file
komorebic.exe ahk-app-specific-configuration applications.yaml
# Application-specific generated configuration written to C:\Users\LGUG2Z\.config\komorebi\komorebi.generated.ahk
#
# You can include the generated configuration at the top of your komorebi.ahk config with this line:
#
# #Include %A_ScriptDir%\komorebi.generated.ahk
# Optionally, provide an override file that follows the same schema as the second argument
komorebic.exe ahk-app-specific-configuration applications.yaml overrides.yaml
```
#### Setting a Custom KOMOREBI_CONFIG_HOME Directory
If you do not want to keep _komorebi_-related files in your `$Env:UserProfile` directory, you can specify a custom directory
by setting the `$Env:KOMOREBI_CONFIG_HOME` environment variable.
For example, to use the `~/.config/komorebi` directory:
```powershell
# Run this command to make sure that the directory has been created
mkdir -p ~/.config/komorebi
# Run this command to open up your PowerShell profile configuration in Notepad
notepad $PROFILE
# Add this line (with your login user!) to the bottom of your PowerShell profile configuration
$Env:KOMOREBI_CONFIG_HOME = 'C:\Users\LGUG2Z\.config\komorebi'
# Save the changes and then reload the PowerShell profile
. $PROFILE
```
If you already have configuration files that you wish to keep, move them to the `~/.config/komorebi` directory.
The next time you run `komorebic start`, any files created by or loaded by _komorebi_ will be placed or expected to
exist in this folder.
#### Floating Windows
Sometimes you will want a specific application to never be tiled, and instead float all the time. You add add rules to
@@ -173,6 +254,24 @@ komorebic.exe identify-tray-application exe Discord.exe
# komorebic.exe identify-tray-application title [TITLE]
```
#### Microsoft Office Applications
Microsoft Office applications such as Word and Excel require certain configuration options to be set in order to be
managed correctly. Below is an example of configuring Microsoft Word to be managed correctly by _komorebi_.
```powershell
# This only needs to be added once
komorebic.exe float-rule class _WwB
# Repeat these for other office applications such as EXCEL.EXE etc
# Note that the capitalised EXE is important here- double check the
# exact case for the name and the file extension in Task Manager or
# the AHK Window Spy
komorebic.exe identify-layered-application exe WINWORD.EXE
komorebic.exe identify-border-overflow-application exe WINWORD.EXE
```
#### Focus Follows Mouse
`komorebi` supports two focus-follows-mouse implementations; the native Windows Xmouse implementation, which treats the
@@ -276,6 +375,31 @@ YAML
configuration: Horizontal
```
#### Dynamically Changing Layouts Based on Number of Visible Window Containers
With `komorebi` it is possible to define rules to automatically change the layout on a specified workspace when a
threshold of window containers is met.
```powershell
# On the first workspace of the first monitor (0 0)
# When there are one or more window containers visible on the screen (1)
# Use the bsp layout (bsp)
komorebic workspace-layout-rule 0 0 1 bsp
# On the first workspace of the first monitor (0 0)
# When there are five or more window containers visible on the screen (five)
# Use the custom layout stored in the home directory (~/custom.yaml)
komorebic workspace-custom-layout-rule 0 0 5 ~/custom.yaml
```
However, if you add workspace layout rules, you will not be able to manually change the layout of a workspace until all
layout rules for that workspace have been cleared.
```powershell
# If you decide that workspace layout rules are not for you, you can remove them from that same workspace like this
komorebic clear-workspace-layout-rules 0 0
```
## Configuration with `komorebic`
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
@@ -287,77 +411,87 @@ 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
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)
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
send-to-monitor-workspace Send the focused window to the specified monitor 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-layout-rule Add a dynamic layout rule for the specified workspace
workspace-custom-layout-rule Add a dynamic custom layout for the specified workspace
clear-workspace-layout-rules Clear all dynamic layout rules 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
unmanaged-window-operation-behaviour Set the operation behaviour when the focused window is not managed
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-object-name-change-application Identify an application that sends EVENT_OBJECT_NAMECHANGE on launch
identify-tray-application Identify an application that closes to the system tray
identify-layered-application Identify an application that has WS_EX_LAYERED, but should still be managed
identify-border-overflow-application 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
ahk-app-specific-configuration Generate common app-specific configurations and fixes to use in komorebi.ahk
format-app-specific-configuration Format a YAML file for use with the 'ahk-app-specific-configuration' command
notification-schema Generate a JSON Schema of subscription notifications
help Print this message or the help of the given subcommand(s)
```
### AutoHotKey Helper Library for `komorebic`
@@ -401,6 +535,7 @@ used [is available here](komorebi.sample.with.lib.ahk).
- [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] Dynamically select layout based on the number of open windows
- [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
@@ -516,3 +651,10 @@ 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).
### Subscription Event Notification Schema
A [JSON Schema](https://json-schema.org/) of the event notifications emitted to subscribers can be generated with
the `komorebic notification-schema` command. The output of this command can be redirected to the clipboard or a file,
which can be used with services such as [Quicktype](https://app.quicktype.io/) to generate type definitions in different
programming languages.

View File

@@ -19,7 +19,7 @@ install:
just install-komorebic
just install-komorebi
komorebic ahk-library
cat '%USERPROFILE%\komorebic.lib.ahk' > komorebic.lib.sample.ahk
cat '%USERPROFILE%\.config\komorebi\komorebic.lib.ahk' > komorebic.lib.sample.ahk
run:
just install-komorebic

31
komorebi-bar/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "komorebi-bar"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi = { path = "../komorebi" }
komorebi-core = { path = "../komorebi-core" }
as-any = "0.3"
chrono = "0.4"
color-eyre = "0.6"
eframe = "0.17"
egui = "0.17"
lazy_static = "1.4"
miow = "0.4"
schemafy = "0.6"
serde = "1"
serde_json = "1"
parking_lot = "0.12"
local-ip-address = "0.4"
clipboard-win = "4.4"
sysinfo = "0.23"
[dependencies.windows]
version = "0.35"
features = [
"Win32_Graphics_Gdi",
]

125
komorebi-bar/src/bar.rs Normal file
View File

@@ -0,0 +1,125 @@
use crate::date::Date;
use crate::ram::Ram;
use crate::time::Time;
use crate::widget::BarWidget;
use crate::widget::Output;
use crate::widget::Widget;
use crate::IpAddress;
use crate::Storage;
use crate::Workspaces;
use clipboard_win::set_clipboard_string;
use color_eyre::owo_colors::OwoColorize;
use eframe::epi::App;
use eframe::epi::Frame;
use egui::style::Margin;
use egui::CentralPanel;
use egui::Color32;
use egui::Context;
use egui::Direction;
use egui::Layout;
use egui::Rounding;
use std::process::Command;
use std::sync::atomic::Ordering;
pub struct Bar {
pub background_rgb: Color32,
pub text_rgb: Color32,
pub workspaces: Workspaces,
pub time: Time,
pub date: Date,
pub ip_address: IpAddress,
pub memory: Ram,
pub storage: Storage,
}
impl App for Bar {
fn update(&mut self, ctx: &Context, frame: &Frame) {
let custom_frame = egui::Frame {
margin: Margin::symmetric(8.0, 8.0),
rounding: Rounding::none(),
fill: self.background_rgb,
..Default::default()
};
CentralPanel::default().frame(custom_frame).show(ctx, |ui| {
ui.horizontal(|horizontal| {
horizontal.style_mut().visuals.override_text_color = Option::from(self.text_rgb);
horizontal.with_layout(Layout::left_to_right(), |ltr| {
for (i, workspace) in self.workspaces.output().iter().enumerate() {
if workspace == "komorebi offline" {
ltr.label(workspace);
} else {
ctx.request_repaint();
if ltr
.selectable_label(*self.workspaces.selected.lock() == i, workspace)
.clicked()
{
let mut selected = self.workspaces.selected.lock();
*selected = i;
if let Err(error) = Workspaces::focus(i) {
eprintln!("{}", error)
};
}
}
}
});
horizontal.with_layout(Layout::right_to_left(), |rtl| {
for time in self.time.output() {
ctx.request_repaint();
if rtl.button(format!("🕐 {}", time)).clicked() {
self.time.format.toggle()
};
}
for date in self.date.output() {
if rtl.button(format!("📅 {}", date)).clicked() {
self.date.format.next()
};
}
for memory in self.memory.output() {
if rtl.button(format!("🐏 {}", memory)).clicked() {
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).output()
{
eprintln!("{}", error)
}
};
}
for disk in self.storage.output() {
if rtl.button(format!("🖴 {}", disk)).clicked() {
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
disk.split(' ').collect::<Vec<&str>>()[0],
])
.output()
{
eprintln!("{}", error)
}
};
}
for ip in self.ip_address.output() {
if rtl.button(format!("🌐 {}", ip)).clicked() {
if let Err(error) =
Command::new("cmd.exe").args(["/C", "ncpa.cpl"]).output()
{
eprintln!("{}", error)
}
};
}
});
})
});
}
fn name(&self) -> &str {
"komorebi-bar"
}
}

46
komorebi-bar/src/date.rs Normal file
View File

@@ -0,0 +1,46 @@
use crate::widget::BarWidget;
pub enum DateFormat {
MonthDateYear,
YearMonthDate,
DateMonthYear,
DayDateMonthYear,
}
impl DateFormat {
pub fn next(&mut self) {
match self {
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
};
}
fn fmt_string(&self) -> String {
match self {
DateFormat::MonthDateYear => String::from("%D"),
DateFormat::YearMonthDate => String::from("%F"),
DateFormat::DateMonthYear => String::from("%v"),
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
}
}
}
pub struct Date {
pub format: DateFormat,
}
impl Date {
pub fn init(format: DateFormat) -> Self {
Self { format }
}
}
impl BarWidget for Date {
fn output(&mut self) -> Vec<String> {
vec![chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()]
}
}

View File

@@ -0,0 +1,27 @@
use crate::widget::BarWidget;
use local_ip_address::find_ifa;
use local_ip_address::local_ip;
pub struct IpAddress {
pub interface: String,
}
impl IpAddress {
pub fn init(interface: String) -> Self {
IpAddress { interface }
}
}
impl BarWidget for IpAddress {
fn output(&mut self) -> Vec<String> {
if let Ok(interfaces) = local_ip_address::list_afinet_netifas() {
if let Some((interface, ip_address)) =
local_ip_address::find_ifa(interfaces, &self.interface)
{
return vec![format!("{}: {}", interface, ip_address)];
}
}
vec![format!("{}: disconnected", self.interface)]
}
}

80
komorebi-bar/src/main.rs Normal file
View File

@@ -0,0 +1,80 @@
mod bar;
mod date;
mod ip_address;
mod ram;
mod storage;
mod time;
mod widget;
mod workspaces;
use crate::ip_address::IpAddress;
use crate::ram::Ram;
use crate::storage::Storage;
use bar::Bar;
use color_eyre::Result;
use date::Date;
use date::DateFormat;
use eframe::run_native;
use eframe::NativeOptions;
use egui::Color32;
use egui::Pos2;
use egui::Vec2;
use komorebi::WindowsApi;
use time::Time;
use time::TimeFormat;
use windows::Win32::Graphics::Gdi::HMONITOR;
use workspaces::Workspaces;
fn main() -> Result<()> {
let workspaces = Workspaces::init(0)?;
let time = Time::init(TimeFormat::TwentyFourHour);
let date = Date::init(DateFormat::DayDateMonthYear);
let ip_address = IpAddress::init(String::from("Ethernet"));
let app = Bar {
background_rgb: Color32::from_rgb(255, 0, 0),
text_rgb: Color32::from_rgb(255, 255, 255),
workspaces,
time,
date,
ip_address,
memory: Ram,
storage: Storage,
};
let mut win_option = NativeOptions {
decorated: false,
..Default::default()
};
// let hmonitors = WindowsApi::valid_hmonitors()?;
// for hmonitor in hmonitors {
// let info = WindowsApi::monitor_info(hmonitor)?;
// }
let info = WindowsApi::monitor_info_w(HMONITOR(65537))?;
let offset = Offsets {
vertical: 10.0,
horizontal: 200.0,
};
win_option.initial_window_pos = Option::from(Pos2::new(
info.rcWork.left as f32 + offset.horizontal,
info.rcWork.top as f32 + offset.vertical * 2.0,
));
win_option.initial_window_size = Option::from(Vec2::new(
info.rcWork.right as f32 - (offset.horizontal * 2.0),
info.rcWork.top as f32 - offset.vertical,
));
win_option.always_on_top = true;
run_native(Box::new(app), win_option);
}
struct Offsets {
vertical: f32,
horizontal: f32,
}

15
komorebi-bar/src/ram.rs Normal file
View File

@@ -0,0 +1,15 @@
use crate::widget::BarWidget;
use sysinfo::RefreshKind;
use sysinfo::System;
use sysinfo::SystemExt;
pub struct Ram;
impl BarWidget for Ram {
fn output(&mut self) -> Vec<String> {
let sys = System::new_with_specifics(RefreshKind::new().with_memory());
let used = sys.used_memory();
let total = sys.total_memory();
vec![format!("RAM: {}%", (used * 100) / total)]
}
}

View File

@@ -0,0 +1,35 @@
use crate::widget::BarWidget;
use crate::widget::Output;
use crate::widget::Widget;
use color_eyre::Result;
use sysinfo::DiskExt;
use sysinfo::RefreshKind;
use sysinfo::System;
use sysinfo::SystemExt;
pub struct Storage;
impl BarWidget for Storage {
fn output(&mut self) -> Vec<String> {
let sys = System::new_with_specifics(RefreshKind::new().with_disks_list());
let mut disks = vec![];
for disk in sys.disks() {
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
disks.push(format!(
"{} {}%",
mount.to_string_lossy(),
(used * 100) / total
))
}
disks.reverse();
disks
}
}

40
komorebi-bar/src/time.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::widget::BarWidget;
pub enum TimeFormat {
TwelveHour,
TwentyFourHour,
}
impl TimeFormat {
pub fn toggle(&mut self) {
match self {
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
};
}
fn fmt_string(&self) -> String {
match self {
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
TimeFormat::TwentyFourHour => String::from("%T"),
}
}
}
pub struct Time {
pub format: TimeFormat,
}
impl Time {
pub fn init(format: TimeFormat) -> Self {
Self { format }
}
}
impl BarWidget for Time {
fn output(&mut self) -> Vec<String> {
vec![chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()]
}
}

View File

@@ -0,0 +1,25 @@
use as_any::AsAny;
use color_eyre::Result;
#[derive(Debug, Clone)]
pub enum Output {
SingleBox(String),
MultiBox(Vec<String>),
}
#[derive(Debug, Copy, Clone)]
pub enum RepaintStrategy {
Default,
Constant,
}
pub trait Widget: AsAny {
fn output(&mut self) -> Result<Output>;
fn repaint_strategy(&self) -> RepaintStrategy {
RepaintStrategy::Default
}
}
pub trait BarWidget {
fn output(&mut self) -> Vec<String>;
}

View File

@@ -0,0 +1,179 @@
use crate::widget::BarWidget;
use color_eyre::Report;
use color_eyre::Result;
use komorebi::Notification;
use komorebi::State;
use miow::pipe::NamedPipe;
use parking_lot::Mutex;
use std::io::Read;
use std::process::Command;
use std::sync::Arc;
use std::thread;
use std::thread::sleep;
use std::time::Duration;
pub struct Workspaces {
pub enabled: bool,
pub monitor_idx: usize,
pub connected: Arc<Mutex<bool>>,
pub pipe: Arc<Mutex<NamedPipe>>,
pub state: Arc<Mutex<State>>,
pub selected: Arc<Mutex<usize>>,
}
impl BarWidget for Workspaces {
fn output(&mut self) -> Vec<String> {
let state = self.state.lock();
let mut workspaces = vec![];
if let Some(primary_monitor) = state.monitors.elements().get(self.monitor_idx) {
for (i, workspace) in primary_monitor.workspaces().iter().enumerate() {
workspaces.push(if let Some(name) = workspace.name() {
name.clone()
} else {
format!("{}", i + 1)
});
}
}
if workspaces.is_empty() || !*self.connected.lock() {
vec!["komorebi offline".to_string()]
} else {
workspaces
}
}
}
const PIPE: &str = r#"\\.\pipe\"#;
impl Workspaces {
pub fn focus(index: usize) -> Result<()> {
Ok(Command::new("cmd.exe")
.args([
"/C",
"komorebic.exe",
"focus-workspace",
&format!("{}", index),
])
.output()
.map(|_| ())?)
}
pub fn init(monitor_idx: usize) -> Result<Self> {
let name = format!("bar-{}", monitor_idx);
let pipe = format!("{}\\{}", PIPE, name);
let mut named_pipe = NamedPipe::new(pipe)?;
let mut output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", &name])
.output()?;
while !output.status.success() {
println!(
"komorebic.exe failed with error code {:?}, retrying in 5 seconds...",
output.status.code()
);
sleep(Duration::from_secs(5));
output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", &name])
.output()?;
}
named_pipe.connect()?;
let mut buf = vec![0; 4096];
let mut bytes_read = named_pipe.read(&mut buf)?;
let mut data = String::from_utf8(buf[0..bytes_read].to_vec())?;
while data == "\n" {
bytes_read = named_pipe.read(&mut buf)?;
data = String::from_utf8(buf[0..bytes_read].to_vec())?;
}
let notification: Notification = serde_json::from_str(&data)?;
let mut workspaces = Self {
enabled: true,
monitor_idx,
connected: Arc::new(Mutex::new(true)),
pipe: Arc::new(Mutex::new(named_pipe)),
state: Arc::new(Mutex::new(notification.state)),
selected: Arc::new(Mutex::new(0)),
};
workspaces.listen()?;
Ok(workspaces)
}
pub fn listen(&mut self) -> Result<()> {
let state = self.state.clone();
let pipe = self.pipe.clone();
let connected = self.connected.clone();
let selected = self.selected.clone();
thread::spawn(move || -> Result<()> {
let mut buf = vec![0; 4096];
loop {
let mut named_pipe = pipe.lock();
match (*named_pipe).read(&mut buf) {
Ok(bytes_read) => {
let data = String::from_utf8(buf[0..bytes_read].to_vec())?;
if data == "\n" {
continue;
}
let notification: Notification = serde_json::from_str(&data)?;
let mut sl = selected.lock();
*sl = notification.state.monitors.elements()[0].focused_workspace_idx();
let mut st = state.lock();
*st = notification.state;
}
Err(error) => {
// Broken pipe
if error.raw_os_error().unwrap() == 109 {
{
let mut cn = connected.lock();
*cn = false;
}
named_pipe.disconnect()?;
let mut output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", "bar"])
.output()?;
while !output.status.success() {
println!(
"komorebic.exe failed with error code {:?}, retrying in 5 seconds...",
output.status.code()
);
sleep(Duration::from_secs(5));
output = Command::new("cmd.exe")
.args(["/C", "komorebic.exe", "subscribe", "bar"])
.output()?;
}
named_pipe.connect()?;
{
let mut cn = connected.lock();
*cn = true;
}
} else {
return Err(Report::from(error));
}
}
}
}
});
Ok(())
}
}

View File

@@ -1,20 +1,21 @@
[package]
name = "komorebi-core"
version = "0.1.7"
version = "0.1.8"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "3", features = ["derive"] }
color-eyre = "0.5"
color-eyre = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.8"
strum = { version = "0.23", features = ["derive"] }
strum = { version = "0.24", features = ["derive"] }
schemars = "0.8"
[dependencies.windows]
version = "0.30"
version = "0.35"
features = [
"Win32_Foundation",
]

View File

@@ -1,6 +1,7 @@
use std::num::NonZeroUsize;
use clap::ArgEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -36,16 +37,16 @@ impl Arrangement for DefaultLayout {
) -> Vec<Rect> {
let len = usize::from(len);
let mut dimensions = match self {
DefaultLayout::BSP => recursive_fibonacci(
Self::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 => {
Self::Columns => columns(area, len),
Self::Rows => rows(area, len),
Self::VerticalStack => {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
@@ -87,7 +88,7 @@ impl Arrangement for DefaultLayout {
layouts
}
DefaultLayout::HorizontalStack => {
Self::HorizontalStack => {
let mut layouts: Vec<Rect> = vec![];
let bottom = match len {
@@ -129,7 +130,7 @@ impl Arrangement for DefaultLayout {
layouts
}
DefaultLayout::UltrawideVerticalStack => {
Self::UltrawideVerticalStack => {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
@@ -341,7 +342,7 @@ impl Arrangement for CustomLayout {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum Axis {
Horizontal,
@@ -474,6 +475,7 @@ fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Optio
cleaned_resize_adjustments
}
#[allow(clippy::only_used_in_recursion)]
fn recursive_fibonacci(
idx: usize,
count: usize,

View File

@@ -0,0 +1,176 @@
use clap::ArgEnum;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::ApplicationIdentifier;
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ApplicationOptions {
ObjectNameChange,
Layered,
BorderOverflow,
TrayAndMultiWindow,
Force,
}
impl ApplicationOptions {
#[must_use]
pub fn cfgen(&self, kind: &ApplicationIdentifier, id: &str) -> String {
format!(
"Run, {}, , Hide",
match self {
ApplicationOptions::ObjectNameChange => {
format!(
"komorebic.exe identify-object-name-change-application {} \"{}\"",
kind, id
)
}
ApplicationOptions::Layered => {
format!(
"komorebic.exe identify-layered-application {} \"{}\"",
kind, id
)
}
ApplicationOptions::BorderOverflow => {
format!(
"komorebic.exe identify-border-overflow-application {} \"{}\"",
kind, id
)
}
ApplicationOptions::TrayAndMultiWindow => {
format!(
"komorebic.exe identify-tray-application {} \"{}\"",
kind, id
)
}
ApplicationOptions::Force => {
format!("komorebic.exe manage-rule {} \"{}\"", kind, id)
}
}
)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifier {
kind: ApplicationIdentifier,
id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifierAndComment {
kind: ApplicationIdentifier,
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
comment: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ApplicationConfiguration {
name: String,
identifier: IdWithIdentifier,
#[serde(skip_serializing_if = "Option::is_none")]
options: Option<Vec<ApplicationOptions>>,
#[serde(skip_serializing_if = "Option::is_none")]
float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ApplicationConfigurationGenerator;
impl ApplicationConfigurationGenerator {
fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
Ok(serde_yaml::from_str(content)?)
}
pub fn format(content: &str) -> Result<String> {
let mut cfgen = Self::load(content)?;
cfgen.sort_by(|a, b| a.name.cmp(&b.name));
Ok(serde_yaml::to_string(&cfgen)?)
}
fn merge(base_content: &str, override_content: &str) -> Result<Vec<ApplicationConfiguration>> {
let base_cfgen = Self::load(base_content)?;
let override_cfgen = Self::load(override_content)?;
let mut final_cfgen = base_cfgen.clone();
for entry in override_cfgen {
let mut replace_idx = None;
for (idx, base_entry) in base_cfgen.iter().enumerate() {
if base_entry.name == entry.name {
replace_idx = Option::from(idx);
}
}
match replace_idx {
None => final_cfgen.push(entry),
Some(idx) => final_cfgen[idx] = entry,
}
}
Ok(final_cfgen)
}
pub fn generate_ahk(base_content: &str, override_content: Option<&str>) -> Result<Vec<String>> {
let mut cfgen = if let Some(override_content) = override_content {
Self::merge(base_content, override_content)?
} else {
Self::load(base_content)?
};
cfgen.sort_by(|a, b| a.name.cmp(&b.name));
let mut lines = vec![
String::from("; Generated by komorebic.exe"),
String::from("; To use this file, add the line below to the top of your komorebi.ahk configuration file"),
String::from("; #Include %A_ScriptDir%\\komorebi.generated.ahk"),
String::from("")
];
let mut float_rules = vec![];
for app in cfgen {
lines.push(format!("; {}", app.name));
if let Some(options) = app.options {
for opt in options {
if let ApplicationOptions::TrayAndMultiWindow = opt {
lines.push(String::from("; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line"));
}
lines.push(opt.cfgen(&app.identifier.kind, &app.identifier.id));
}
}
if let Some(float_identifiers) = app.float_identifiers {
for float in float_identifiers {
let float_rule = format!(
"Run, komorebic.exe float-rule {} \"{}\", , Hide",
float.kind, float.id
);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if let Some(comment) = float.comment {
lines.push(format!("; {}", comment));
};
lines.push(float_rule);
}
}
}
lines.push(String::from(""));
}
Ok(lines)
}
}

View File

@@ -7,12 +7,13 @@ use std::path::PathBuf;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::Rect;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CustomLayout(Vec<Column>);
impl Deref for CustomLayout {
@@ -251,7 +252,7 @@ impl CustomLayout {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "column", content = "configuration")]
pub enum Column {
Primary(Option<ColumnWidth>),
@@ -259,18 +260,18 @@ pub enum Column {
Tertiary(ColumnSplit),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnWidth {
WidthPercentage(usize),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnSplit {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnSplitWithCapacity {
Horizontal(usize),
Vertical(usize),

View File

@@ -1,12 +1,13 @@
use std::num::NonZeroUsize;
use clap::ArgEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum CycleDirection {
Previous,

View File

@@ -1,4 +1,5 @@
use clap::ArgEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -8,7 +9,7 @@ use crate::OperationDirection;
use crate::Rect;
use crate::Sizing;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum DefaultLayout {
BSP,
@@ -21,7 +22,7 @@ pub enum DefaultLayout {
impl DefaultLayout {
#[must_use]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
pub fn resize(
&self,
unaltered: &Rect,

View File

@@ -72,34 +72,34 @@ impl Direction for DefaultLayout {
) -> 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,
Self::BSP => count > 2 && idx != 0 && idx != 1,
Self::Columns => false,
Self::Rows | Self::HorizontalStack => idx != 0,
Self::VerticalStack => idx != 0 && idx != 1,
Self::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,
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
Self::Columns => false,
Self::Rows => idx != count - 1,
Self::VerticalStack => idx != 0 && idx != count - 1,
Self::HorizontalStack => idx == 0,
Self::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,
Self::BSP => count > 1 && idx != 0,
Self::Columns | Self::VerticalStack => idx != 0,
Self::Rows => false,
Self::HorizontalStack => idx != 0 && idx != 1,
Self::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 {
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
Self::Columns => idx != count - 1,
Self::Rows => false,
Self::VerticalStack => idx == 0,
Self::HorizontalStack => idx != 0 && idx != count - 1,
Self::UltrawideVerticalStack => match count {
0 | 1 => false,
2 => idx != 0,
_ => idx < 2,
@@ -110,45 +110,40 @@ impl Direction for DefaultLayout {
fn up_index(&self, idx: usize) -> usize {
match self {
DefaultLayout::BSP => {
Self::BSP => {
if idx % 2 == 0 {
idx - 1
} else {
idx - 2
}
}
DefaultLayout::Columns => unreachable!(),
DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::UltrawideVerticalStack => idx - 1,
DefaultLayout::HorizontalStack => 0,
Self::Columns => unreachable!(),
Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx - 1,
Self::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,
Self::BSP | Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx + 1,
Self::Columns => unreachable!(),
Self::HorizontalStack => 1,
}
}
fn left_index(&self, idx: usize) -> usize {
match self {
DefaultLayout::BSP => {
Self::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 {
Self::Columns | Self::HorizontalStack => idx - 1,
Self::Rows => unreachable!(),
Self::VerticalStack => 0,
Self::UltrawideVerticalStack => match idx {
0 => 1,
1 => unreachable!(),
_ => 0,
@@ -158,10 +153,10 @@ impl Direction for DefaultLayout {
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 {
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
Self::Rows => unreachable!(),
Self::VerticalStack => 1,
Self::UltrawideVerticalStack => match idx {
1 => 0,
0 => 2,
_ => unreachable!(),

View File

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

View File

@@ -1,11 +1,12 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_errors_doc, clippy::use_self)]
use std::path::PathBuf;
use std::str::FromStr;
use clap::ArgEnum;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -22,6 +23,7 @@ pub use operation_direction::OperationDirection;
pub use rect::Rect;
pub mod arrangement;
pub mod config_generation;
pub mod custom_layout;
pub mod cycle_direction;
pub mod default_layout;
@@ -30,7 +32,7 @@ pub mod layout;
pub mod operation_direction;
pub mod rect;
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
#[derive(Clone, Debug, Serialize, Deserialize, Display, JsonSchema)]
#[serde(tag = "type", content = "content")]
pub enum SocketMessage {
// Window / Container Commands
@@ -47,6 +49,7 @@ pub enum SocketMessage {
MoveContainerToWorkspaceNumber(usize),
SendContainerToMonitorNumber(usize),
SendContainerToWorkspaceNumber(usize),
SendContainerToMonitorWorkspaceNumber(usize, usize),
MoveWorkspaceToMonitorNumber(usize),
Promote,
ToggleFloat,
@@ -54,6 +57,7 @@ pub enum SocketMessage {
ToggleMaximize,
ToggleWindowContainerBehaviour,
WindowHidingBehaviour(HidingBehaviour),
UnmanagedWindowOperationBehaviour(OperationBehaviour),
// Current Workspace Commands
ManageFocusedWindow,
UnmanageFocusedWindow,
@@ -84,6 +88,9 @@ pub enum SocketMessage {
WorkspaceName(usize, usize, String),
WorkspaceLayout(usize, usize, DefaultLayout),
WorkspaceLayoutCustom(usize, usize, PathBuf),
WorkspaceLayoutRule(usize, usize, usize, DefaultLayout),
WorkspaceLayoutCustomRule(usize, usize, usize, PathBuf),
ClearWorkspaceLayoutRules(usize, usize),
// Configuration
ReloadConfiguration,
WatchConfiguration(bool),
@@ -93,8 +100,10 @@ pub enum SocketMessage {
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
FloatRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
IdentifyTrayApplication(ApplicationIdentifier, String),
IdentifyBorderOverflow(ApplicationIdentifier, String),
IdentifyLayeredApplication(ApplicationIdentifier, String),
IdentifyBorderOverflowApplication(ApplicationIdentifier, String),
State,
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
@@ -103,6 +112,7 @@ pub enum SocketMessage {
ToggleMouseFollowsFocus,
AddSubscriber(String),
RemoveSubscriber(String),
NotificationSchema,
}
impl SocketMessage {
@@ -119,7 +129,7 @@ impl FromStr for SocketMessage {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum StateQuery {
FocusedMonitorIndex,
@@ -128,36 +138,44 @@ pub enum StateQuery {
FocusedWindowIndex,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ApplicationIdentifier {
Exe,
Class,
Title,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum FocusFollowsMouseImplementation {
Komorebi,
Windows,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum WindowContainerBehaviour {
Create,
Append,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum HidingBehaviour {
Hide,
Minimize,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum OperationBehaviour {
Op,
NoOp,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum Sizing {
Increase,
@@ -168,8 +186,8 @@ impl Sizing {
#[must_use]
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
match self {
Sizing::Increase => value + adjustment,
Sizing::Decrease => {
Self::Increase => value + adjustment,
Self::Decrease => {
if value > 0 && value - adjustment >= 0 {
value - adjustment
} else {

View File

@@ -1,6 +1,7 @@
use std::num::NonZeroUsize;
use clap::ArgEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -9,7 +10,7 @@ use strum::EnumString;
use crate::direction::Direction;
use crate::Axis;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
#[strum(serialize_all = "snake_case")]
pub enum OperationDirection {
Left,

View File

@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use windows::Win32::Foundation::RECT;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
pub struct Rect {
pub left: i32,
pub top: i32,

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.7"
version = "0.1.8"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -8,6 +8,14 @@ repository = "https://github.com/LGUG2Z/komorebi"
license = "MIT"
edition = "2021"
[lib]
name = "komorebi"
path = "src/lib.rs"
[[bin]]
name = "komorebi"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
@@ -15,7 +23,7 @@ komorebi-core = { path = "../komorebi-core" }
bitflags = "1"
clap = { version = "3", features = ["derive"] }
color-eyre = "0.5"
color-eyre = "0.6"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
ctrlc = "3"
@@ -24,12 +32,12 @@ getset = "0.1"
hotwatch = "0.4"
lazy_static = "1"
nanoid = "0.4"
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
paste = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
strum = { version = "0.23", features = ["derive"] }
sysinfo = "0.22"
strum = { version = "0.24", features = ["derive"] }
sysinfo = "0.23"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ -38,9 +46,10 @@ which = "4"
winput = "0.2"
miow = "0.4"
winreg = "0.10"
schemars = "0.8"
[dependencies.windows]
version = "0.30"
version = "0.35"
features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",

View File

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

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

@@ -0,0 +1,236 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use which::which;
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
use komorebi_core::HidingBehaviour;
use komorebi_core::SocketMessage;
#[macro_use]
mod ring;
mod container;
mod monitor;
mod process_command;
mod process_event;
mod process_movement;
mod set_window_position;
mod styles;
mod window;
mod window_manager;
mod window_manager_event;
mod windows_api;
mod windows_callbacks;
mod winevent;
mod winevent_listener;
mod workspace;
pub use process_command::listen_for_commands;
pub use process_event::listen_for_events;
pub use process_movement::listen_for_movements;
pub use window_manager::State;
pub use window_manager::WindowManager;
pub use window_manager_event::WindowManagerEvent;
pub use windows_api::WindowsApi;
pub use winevent_listener::WinEventListener;
lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![
"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(),
]));
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![
// 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 ref HOME_DIR: PathBuf = {
if let Ok(home_path) = std::env::var("KOMOREBI_CONFIG_HOME") {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
}
} else {
dirs::home_dir().expect("there is no home directory")
}
};
}
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
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
}
pub fn load_configuration() -> Result<()> {
let home = HOME_DIR.clone();
let mut config_v1 = home.clone();
config_v1.push("komorebi.ahk");
let mut config_v2 = home;
config_v2.push("komorebi.ahk2");
if config_v1.exists() && which("autohotkey.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v1
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("autohotkey.exe")
.arg(config_v1.as_os_str())
.output()?;
} else if config_v2.exists() && which("AutoHotkey64.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v2
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("AutoHotkey64.exe")
.arg(config_v2.as_os_str())
.output()?;
};
Ok(())
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Notification {
pub event: NotificationEvent,
pub state: State,
}
fn notify_subscribers(notification: &str) {
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);
}
}

View File

@@ -1,107 +1,37 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![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 clap::Parser;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
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 clap::Parser;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
use serde::Serialize;
use sysinfo::Process;
use sysinfo::ProcessExt;
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;
#[macro_use]
mod ring;
mod container;
mod monitor;
mod process_command;
mod process_event;
mod process_movement;
mod set_window_position;
mod styles;
mod window;
mod window_manager;
mod window_manager_event;
mod windows_api;
mod windows_callbacks;
mod winevent;
mod winevent_listener;
mod workspace;
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_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(),
]));
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![
// 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);
use komorebi::listen_for_commands;
use komorebi::listen_for_events;
use komorebi::listen_for_movements;
use komorebi::load_configuration;
use komorebi::WinEventListener;
use komorebi::WindowManager;
use komorebi::WindowManagerEvent;
use komorebi::WindowsApi;
use komorebi::CUSTOM_FFM;
use komorebi::HOME_DIR;
use komorebi::SESSION_ID;
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
@@ -114,7 +44,7 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
std::env::set_var("RUST_LOG", "info");
}
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let home = HOME_DIR.clone();
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);
@@ -167,134 +97,6 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
Ok((guard, color_guard))
}
pub fn load_configuration() -> Result<()> {
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");
let mut config_v2 = home;
config_v2.push("komorebi.ahk2");
if config_v1.exists() && which("autohotkey.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v1
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("autohotkey.exe")
.arg(config_v1.as_os_str())
.output()?;
} else if config_v2.exists() && which("AutoHotkey64.exe").is_ok() {
tracing::info!(
"loading configuration file: {}",
config_v2
.as_os_str()
.to_str()
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("AutoHotkey64.exe")
.arg(config_v2.as_os_str())
.output()?;
};
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() {
@@ -341,9 +143,20 @@ fn main() -> Result<()> {
let mut system = sysinfo::System::new_all();
system.refresh_processes();
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 matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe").collect();
if matched_procs.len() > 1 {
let mut shim_is_active = false;
for proc in matched_procs {
if proc.root().ends_with("shims") {
shim_is_active = true;
}
}
if !shim_is_active {
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
std::process::exit(1);
}
}
// File logging worker guard has to have an assignment in the main fn to work
@@ -358,7 +171,7 @@ fn main() -> Result<()> {
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
let winevent_listener = WinEventListener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(

View File

@@ -7,6 +7,8 @@ use getset::CopyGetters;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Rect;
@@ -15,7 +17,9 @@ use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
#[derive(
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
id: isize,
@@ -24,7 +28,7 @@ pub struct Monitor {
#[getset(get = "pub", set = "pub")]
work_area_size: Rect,
workspaces: Ring<Workspace>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get_mut = "pub")]
workspace_names: HashMap<usize, String>,
}
@@ -58,10 +62,19 @@ impl Monitor {
Ok(())
}
pub fn add_container(&mut self, container: Container) -> Result<()> {
let workspace = self
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
pub fn add_container(
&mut self,
container: Container,
workspace_idx: Option<usize>,
) -> Result<()> {
let workspace = if let Some(idx) = workspace_idx {
self.workspaces_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
} else {
self.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?
};
workspace.add_container(container);

View File

@@ -13,6 +13,7 @@ use color_eyre::eyre::anyhow;
use color_eyre::Result;
use miow::pipe::connect;
use parking_lot::Mutex;
use schemars::schema_for;
use uds_windows::UnixStream;
use komorebi_core::ApplicationIdentifier;
@@ -37,7 +38,10 @@ use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::CUSTOM_FFM;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::HOME_DIR;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::SUBSCRIPTION_PIPES;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
@@ -187,13 +191,16 @@ impl WindowManager {
self.move_container_to_workspace(workspace_idx, true)?;
}
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, true)?;
self.move_container_to_monitor(monitor_idx, None, 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)?;
self.move_container_to_monitor(monitor_idx, None, false)?;
}
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
}
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
self.move_workspace_to_monitor(monitor_idx)?;
@@ -238,6 +245,35 @@ impl WindowManager {
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?;
}
SocketMessage::WorkspaceLayoutRule(
monitor_idx,
workspace_idx,
at_container_count,
layout,
) => {
self.add_workspace_layout_default_rule(
monitor_idx,
workspace_idx,
at_container_count,
layout,
)?;
}
SocketMessage::WorkspaceLayoutCustomRule(
monitor_idx,
workspace_idx,
at_container_count,
path,
) => {
self.add_workspace_layout_custom_rule(
monitor_idx,
workspace_idx,
at_container_count,
path,
)?;
}
SocketMessage::ClearWorkspaceLayoutRules(monitor_idx, workspace_idx) => {
self.clear_workspace_layout_rules(monitor_idx, workspace_idx)?;
}
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
@@ -306,8 +342,7 @@ impl WindowManager {
Err(error) => error.to_string(),
};
let mut socket =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut socket = HOME_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
@@ -330,8 +365,7 @@ impl WindowManager {
}
.to_string();
let mut socket =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut socket = HOME_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
@@ -517,18 +551,30 @@ impl WindowManager {
SocketMessage::WatchConfiguration(enable) => {
self.watch_configuration(enable)?;
}
SocketMessage::IdentifyBorderOverflow(_, id) => {
SocketMessage::IdentifyBorderOverflowApplication(_, id) => {
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
}
SocketMessage::IdentifyObjectNameChangeApplication(_, id) => {
let mut identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
}
SocketMessage::IdentifyTrayApplication(_, id) => {
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
}
SocketMessage::IdentifyLayeredApplication(_, id) => {
let mut identifiers = LAYERED_WHITELIST.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
}
SocketMessage::ManageFocusedWindow => {
self.manage_focused_window()?;
}
@@ -635,6 +681,19 @@ impl WindowManager {
let mut hiding_behaviour = HIDING_BEHAVIOUR.lock();
*hiding_behaviour = behaviour;
}
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
self.unmanaged_window_operation_behaviour = behaviour;
}
SocketMessage::NotificationSchema => {
let notification = schema_for!(Notification);
let schema = serde_json::to_string_pretty(&notification)?;
let mut socket = HOME_DIR.clone();
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(&socket)?;
stream.write_all(schema.as_bytes())?;
}
};
tracing::info!("processed");
@@ -662,8 +721,8 @@ impl WindowManager {
self.process_command(message.clone())?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: (&*self).into(),
})?)?;
state: self.as_ref().into(),
})?);
}
Ok(())

View File

@@ -20,6 +20,7 @@ use crate::windows_api::WindowsApi;
use crate::Notification;
use crate::NotificationEvent;
use crate::HIDDEN_HWNDS;
use crate::HOME_DIR;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
@@ -75,7 +76,23 @@ impl WindowManager {
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)?;
// This is a hidden window apparently associated with COM support mechanisms (based
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
//
// The hidden window, OLEChannelWnd, associated with this class (spawned by
// explorer.exe), after some debugging, is observed to always be tied to the primary
// display monitor, or (usually) monitor 0 in the WindowManager state.
//
// Due to this, at least one user in the Discord has witnessed behaviour where, when
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
// set repeatedly to 0, regardless of where the current foreground window is actually
// located.
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if window.class()? != "OleMainThreadWndClass" {
self.focus_monitor(monitor_idx)?;
}
}
_ => {}
}
@@ -466,8 +483,7 @@ impl WindowManager {
}
}
let mut hwnd_json =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut hwnd_json = HOME_DIR.clone();
hwnd_json.push("komorebi.hwnd.json");
let file = OpenOptions::new()
.write(true)
@@ -478,8 +494,8 @@ impl WindowManager {
serde_json::to_writer_pretty(&file, &known_hwnds)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::WindowManager(*event),
state: (&*self).into(),
})?)?;
state: self.as_ref().into(),
})?);
tracing::info!("processed: {}", event.window().to_string());
Ok(())

View File

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

View File

@@ -18,20 +18,20 @@ use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
bitflags! {
#[derive(Default)]
pub struct SetWindowPosition: u32 {
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;
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;
}
}

View File

@@ -58,33 +58,33 @@ use windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
bitflags! {
#[derive(Default)]
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;
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;
}
}
@@ -92,32 +92,32 @@ bitflags! {
bitflags! {
#[derive(Default)]
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;
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;
}
}

View File

@@ -4,8 +4,10 @@ use std::fmt::Formatter;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::ser::Error;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use windows::Win32::Foundation::HWND;
@@ -21,11 +23,11 @@ use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
use crate::LAYERED_EXE_WHITELIST;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
pub struct Window {
pub(crate) hwnd: isize,
}
@@ -294,8 +296,8 @@ impl Window {
};
let allow_layered = {
let layered_exe_whitelist = LAYERED_EXE_WHITELIST.lock();
layered_exe_whitelist.contains(&exe_name)
let layered_whitelist = LAYERED_WHITELIST.lock();
layered_whitelist.contains(&exe_name) || layered_whitelist.contains(&class)
};
let allow_wsl2_gui = {

View File

@@ -11,6 +11,8 @@ use crossbeam_channel::Receiver;
use hotwatch::notify::DebouncedEvent;
use hotwatch::Hotwatch;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixListener;
@@ -21,6 +23,7 @@ use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::Layout;
use komorebi_core::OperationBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
@@ -38,8 +41,10 @@ use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
use crate::workspace::Workspace;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::FLOAT_IDENTIFIERS;
use crate::LAYERED_EXE_WHITELIST;
use crate::HOME_DIR;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
@@ -53,6 +58,7 @@ pub struct WindowManager {
pub work_area_offset: Option<Rect>,
pub resize_delta: i32,
pub window_container_behaviour: WindowContainerBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub mouse_follows_focus: bool,
pub hotwatch: Hotwatch,
@@ -61,7 +67,7 @@ pub struct WindowManager {
pub pending_move_op: Option<(usize, usize, usize)>,
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct State {
pub monitors: Ring<Monitor>,
pub is_paused: bool,
@@ -74,9 +80,16 @@ pub struct State {
pub has_pending_raise_op: bool,
pub float_identifiers: Vec<String>,
pub manage_identifiers: Vec<String>,
pub layered_exe_whitelist: Vec<String>,
pub layered_whitelist: Vec<String>,
pub tray_and_multi_window_identifiers: Vec<String>,
pub border_overflow_identifiers: Vec<String>,
pub name_change_on_launch_identifiers: Vec<String>,
}
impl AsRef<Self> for WindowManager {
fn as_ref(&self) -> &Self {
self
}
}
impl From<&WindowManager> for State {
@@ -93,9 +106,10 @@ impl From<&WindowManager> for State {
has_pending_raise_op: wm.has_pending_raise_op,
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().clone(),
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
border_overflow_identifiers: BORDER_OVERFLOW_IDENTIFIERS.lock().clone(),
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
}
}
}
@@ -129,7 +143,7 @@ impl EnforceWorkspaceRuleOp {
impl WindowManager {
#[tracing::instrument]
pub fn new(incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>) -> Result<Self> {
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let home = HOME_DIR.clone();
let mut socket = home;
socket.push("komorebi.sock");
let socket = socket.as_path();
@@ -161,6 +175,7 @@ impl WindowManager {
virtual_desktop_id: current_virtual_desktop(),
work_area_offset: None,
window_container_behaviour: WindowContainerBehaviour::Create,
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
resize_delta: 50,
focus_follows_mouse: None,
mouse_follows_focus: true,
@@ -186,7 +201,7 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn watch_configuration(&mut self, enable: bool) -> Result<()> {
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let home = HOME_DIR.clone();
let mut config_v1 = home.clone();
config_v1.push("komorebi.ahk");
@@ -302,6 +317,7 @@ impl WindowManager {
should_update = true;
}
if reference.size() != monitor.size() {
monitor.set_size(Rect {
left: reference.size().left,
@@ -774,7 +790,29 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn move_container_to_monitor(&mut self, idx: usize, follow: bool) -> Result<()> {
fn handle_unmanaged_window_behaviour(&self) -> Result<()> {
if let OperationBehaviour::NoOp = self.unmanaged_window_operation_behaviour {
let workspace = self.focused_workspace()?;
let focused_hwnd = WindowsApi::foreground_window()?;
if !workspace.contains_managed_window(focused_hwnd) {
return Err(anyhow!(
"ignoring commands while active window is not managed by komorebi"
));
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_container_to_monitor(
&mut self,
monitor_idx: usize,
workspace_idx: Option<usize>,
follow: bool,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("moving container");
let invisible_borders = self.invisible_borders;
@@ -798,17 +836,19 @@ impl WindowManager {
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
monitor.update_focused_workspace(offset, &invisible_borders)?;
let target_monitor = self
.monitors_mut()
.get_mut(idx)
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.add_container(container)?;
target_monitor.add_container(container, workspace_idx)?;
target_monitor.load_focused_workspace(mouse_follows_focus)?;
target_monitor.update_focused_workspace(offset, &invisible_borders)?;
if follow {
self.focus_monitor(idx)?;
self.focus_monitor(monitor_idx)?;
}
self.update_focused_workspace(self.mouse_follows_focus)
@@ -816,6 +856,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("moving container");
let mouse_follows_focus = self.mouse_follows_focus;
@@ -828,6 +870,7 @@ impl WindowManager {
self.update_focused_workspace(mouse_follows_focus)
}
pub fn remove_focused_workspace(&mut self) -> Option<Workspace> {
let focused_monitor: &mut Monitor = self.focused_monitor_mut()?;
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
@@ -859,7 +902,10 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn focus_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("focusing container");
let workspace = self.focused_workspace_mut()?;
let new_idx = workspace
@@ -874,6 +920,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn move_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("moving container");
let workspace = self.focused_workspace_mut()?;
@@ -890,7 +938,22 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn focus_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("focusing container");
let mut maximize_next = false;
let mut monocle_next = false;
if self.focused_workspace_mut()?.maximized_window().is_some() {
maximize_next = true;
self.unmaximize_window()?;
}
if self.focused_workspace_mut()?.monocle_container().is_some() {
monocle_next = true;
self.monocle_off()?;
}
let workspace = self.focused_workspace_mut()?;
let new_idx = workspace
@@ -898,13 +961,22 @@ impl WindowManager {
.ok_or_else(|| anyhow!("this is not a valid direction from the current position"))?;
workspace.focus_container(new_idx);
self.focused_window_mut()?.focus(self.mouse_follows_focus)?;
if maximize_next {
self.toggle_maximize()?;
} else if monocle_next {
self.toggle_monocle()?;
} else {
self.focused_window_mut()?.focus(self.mouse_follows_focus)?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("moving container");
let workspace = self.focused_workspace_mut()?;
@@ -921,6 +993,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn cycle_container_window_in_direction(&mut self, direction: CycleDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("cycling container windows");
let container = self.focused_container_mut()?;
@@ -943,6 +1017,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn add_window_to_container(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("adding window to container");
let workspace = self.focused_workspace_mut()?;
@@ -979,6 +1055,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn promote_container_to_front(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("promoting container");
let workspace = self.focused_workspace_mut()?;
@@ -988,6 +1066,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn remove_window_from_container(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("removing window");
if self.focused_container()?.windows().len() == 1 {
@@ -1060,6 +1140,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn toggle_monocle(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
match workspace.monocle_container() {
@@ -1067,7 +1149,7 @@ impl WindowManager {
Some(_) => self.monocle_off()?,
}
self.update_focused_workspace(false)
self.update_focused_workspace(true)
}
#[tracing::instrument(skip(self))]
@@ -1088,6 +1170,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn toggle_maximize(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
match workspace.maximized_window() {
@@ -1095,7 +1179,7 @@ impl WindowManager {
Some(_) => self.unmaximize_window()?,
}
self.update_focused_workspace(false)
self.update_focused_workspace(true)
}
#[tracing::instrument(skip(self))]
@@ -1260,6 +1344,127 @@ impl WindowManager {
self.update_focused_workspace(false)
}
#[tracing::instrument(skip(self))]
pub fn add_workspace_layout_default_rule(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
at_container_count: usize,
layout: DefaultLayout,
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let focused_workspace_idx = monitor.focused_workspace_idx();
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
rules.retain(|pair| pair.0 != at_container_count);
rules.push((at_container_count, Layout::Default(layout)));
rules.sort_by(|a, b| a.0.cmp(&b.0));
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn add_workspace_layout_custom_rule(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
at_container_count: usize,
path: PathBuf,
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let focused_workspace_idx = monitor.focused_workspace_idx();
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let layout = CustomLayout::from_path_buf(path)?;
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
rules.retain(|pair| pair.0 != at_container_count);
rules.push((at_container_count, Layout::Custom(layout)));
rules.sort_by(|a, b| a.0.cmp(&b.0));
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn clear_workspace_layout_rules(
&mut self,
monitor_idx: usize,
workspace_idx: usize,
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let offset = self.work_area_offset;
let focused_monitor_idx = self.focused_monitor_idx();
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let focused_workspace_idx = monitor.focused_workspace_idx();
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let rules: &mut Vec<(usize, Layout)> = workspace.layout_rules_mut();
rules.clear();
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area, offset, &invisible_borders)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
}
}
#[tracing::instrument(skip(self))]
pub fn set_workspace_layout_default(
&mut self,

View File

@@ -1,13 +1,15 @@
use std::fmt::Display;
use std::fmt::Formatter;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::window::Window;
use crate::winevent::WinEvent;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
#[derive(Debug, Copy, Clone, Serialize)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "content")]
pub enum WindowManagerEvent {
Destroy(WinEvent, Window),
@@ -88,6 +90,7 @@ impl Display for WindowManagerEvent {
}
impl WindowManagerEvent {
#[must_use]
pub const fn window(self) -> Window {
match self {
WindowManagerEvent::Destroy(_, window)
@@ -105,6 +108,7 @@ impl WindowManagerEvent {
}
}
#[must_use]
pub fn from_win_event(winevent: WinEvent, window: Window) -> Option<Self> {
match winevent {
WinEvent::ObjectDestroy => Option::from(Self::Destroy(winevent, window)),
@@ -137,7 +141,10 @@ impl WindowManagerEvent {
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
if object_name_change_on_launch.contains(&window.exe().ok()?) {
if object_name_change_on_launch.contains(&window.exe().ok()?)
|| object_name_change_on_launch.contains(&window.class().ok()?)
|| object_name_change_on_launch.contains(&window.title().ok()?)
{
Option::from(Self::Show(winevent, window))
} else {
None

View File

@@ -1,18 +1,17 @@
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::ffi::c_void;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::Error;
use color_eyre::Result;
use windows::core::Result as WindowsCrateResult;
use windows::core::PWSTR;
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;
@@ -37,6 +36,7 @@ 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_NAME_WIN32;
use windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
@@ -66,6 +66,7 @@ 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::SET_WINDOW_POS_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
@@ -123,15 +124,16 @@ pub trait ProcessWindowsCrateResult<T> {
fn process(self) -> Result<T>;
}
macro_rules! impl_process_windows_crate_result {
macro_rules! impl_process_windows_crate_integer_wrapper_result {
( $($input:ty => $deref:ty),+ $(,)? ) => (
paste::paste! {
$(
impl ProcessWindowsCrateResult<$deref> for WindowsCrateResult<$input> {
impl ProcessWindowsCrateResult<$deref> for $input {
fn process(self) -> Result<$deref> {
match self {
Ok(value) => Ok(value.0),
Err(error) => Err(error.into()),
if self.0 == 0 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(self.0)
}
}
}
@@ -140,7 +142,7 @@ macro_rules! impl_process_windows_crate_result {
);
}
impl_process_windows_crate_result!(
impl_process_windows_crate_integer_wrapper_result!(
HWND => isize,
);
@@ -237,12 +239,14 @@ impl WindowsApi {
.process()
}
#[must_use]
pub fn monitor_from_window(hwnd: HWND) -> 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 { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
}
#[must_use]
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
@@ -265,7 +269,7 @@ impl WindowsApi {
layout.top,
layout.right,
layout.bottom,
flags,
SET_WINDOW_POS_FLAGS(flags),
)
}
.ok()
@@ -295,7 +299,7 @@ impl WindowsApi {
}
pub fn foreground_window() -> Result<isize> {
unsafe { GetForegroundWindow() }.ok().process()
unsafe { GetForegroundWindow() }.process()
}
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
@@ -304,16 +308,16 @@ impl WindowsApi {
#[allow(dead_code)]
pub fn top_window() -> Result<isize> {
unsafe { GetTopWindow(HWND::default()) }.ok().process()
unsafe { GetTopWindow(HWND::default()) }.process()
}
pub fn desktop_window() -> Result<isize> {
unsafe { GetDesktopWindow() }.ok().process()
unsafe { GetDesktopWindow() }.process()
}
#[allow(dead_code)]
pub fn next_window(hwnd: HWND) -> Result<isize> {
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok().process()
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
}
#[allow(dead_code)]
@@ -351,7 +355,7 @@ impl WindowsApi {
}
pub fn window_from_point(point: POINT) -> Result<isize> {
unsafe { WindowFromPoint(point) }.ok().process()
unsafe { WindowFromPoint(point) }.process()
}
pub fn window_at_cursor_pos() -> Result<isize> {
@@ -362,6 +366,7 @@ impl WindowsApi {
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
}
#[must_use]
pub fn window_thread_process_id(hwnd: HWND) -> (u32, u32) {
let mut process_id: u32 = 0;
@@ -372,10 +377,12 @@ impl WindowsApi {
(process_id, thread_id)
}
#[must_use]
pub fn current_thread_id() -> u32 {
unsafe { GetCurrentThreadId() }
}
#[must_use]
pub fn current_process_id() -> u32 {
unsafe { GetCurrentProcessId() }
}
@@ -400,7 +407,7 @@ impl WindowsApi {
}
pub fn set_focus(hwnd: HWND) -> Result<()> {
unsafe { SetFocus(hwnd) }.ok().map(|_| ()).process()
unsafe { SetFocus(hwnd) }.process().map(|_| ())
}
#[allow(dead_code)]
@@ -436,9 +443,7 @@ impl WindowsApi {
pub fn window_text_w(hwnd: HWND) -> Result<String> {
let mut text: [u16; 512] = [0; 512];
match WindowsResult::from(unsafe {
GetWindowTextW(hwnd, PWSTR(text.as_mut_ptr()), text.len().try_into()?)
}) {
match WindowsResult::from(unsafe { GetWindowTextW(hwnd, &mut text) }) {
WindowsResult::Ok(len) => {
let length = usize::try_from(len)?;
Ok(String::from_utf16(&text[..length])?)
@@ -452,9 +457,7 @@ impl WindowsApi {
inherit_handle: bool,
process_id: u32,
) -> Result<HANDLE> {
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }
.ok()
.process()
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }.process()
}
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
@@ -467,7 +470,12 @@ impl WindowsApi {
let text_ptr = path.as_mut_ptr();
unsafe {
QueryFullProcessImageNameW(handle, 0, PWSTR(text_ptr), std::ptr::addr_of_mut!(len))
QueryFullProcessImageNameW(
handle,
PROCESS_NAME_WIN32,
PWSTR(text_ptr),
std::ptr::addr_of_mut!(len),
)
}
.ok()
.process()?;
@@ -488,7 +496,7 @@ impl WindowsApi {
let mut class: [u16; BUF_SIZE] = [0; BUF_SIZE];
let len = Result::from(WindowsResult::from(unsafe {
RealGetWindowClassW(hwnd, PWSTR(class.as_mut_ptr()), u32::try_from(BUF_SIZE)?)
RealGetWindowClassW(hwnd, &mut class)
}))?;
Ok(String::from_utf16(&class[0..len as usize])?)
@@ -529,18 +537,25 @@ impl WindowsApi {
))
}
#[must_use]
pub fn is_window(hwnd: HWND) -> bool {
unsafe { IsWindow(hwnd) }.into()
}
#[must_use]
pub fn is_window_visible(hwnd: HWND) -> bool {
unsafe { IsWindowVisible(hwnd) }.into()
}
#[must_use]
pub fn is_iconic(hwnd: HWND) -> bool {
unsafe { IsIconic(hwnd) }.into()
}
pub fn monitor_info(hmonitor: isize) -> Result<MONITORINFO> {
Self::monitor_info_w(HMONITOR(hmonitor))
}
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFO> {
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
@@ -563,7 +578,7 @@ impl WindowsApi {
}
#[allow(dead_code)]
pub fn system_parameters_info_w(
fn system_parameters_info_w(
action: SYSTEM_PARAMETERS_INFO_ACTION,
ui_param: u32,
pv_param: *mut c_void,

View File

@@ -1,3 +1,7 @@
#![allow(clippy::use_self)]
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
@@ -85,7 +89,7 @@ use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Display)]
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display, JsonSchema)]
#[repr(u32)]
#[allow(dead_code)]
pub enum WinEvent {

View File

@@ -32,14 +32,15 @@ pub struct WinEventListener {
outgoing_events: Arc<Mutex<Sender<WindowManagerEvent>>>,
}
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> WinEventListener {
WinEventListener {
hook: Arc::new(AtomicIsize::new(0)),
outgoing_events: outgoing,
}
}
impl WinEventListener {
#[must_use]
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> Self {
Self {
hook: Arc::new(AtomicIsize::new(0)),
outgoing_events: outgoing,
}
}
pub fn start(self) {
let hook = self.hook.clone();
let outgoing = self.outgoing_events.lock().clone();

View File

@@ -7,6 +7,8 @@ use getset::CopyGetters;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Axis;
@@ -21,32 +23,36 @@ use crate::ring::Ring;
use crate::window::Window;
use crate::windows_api::WindowsApi;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
#[derive(
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
)]
pub struct Workspace {
#[getset(set = "pub")]
#[getset(get = "pub", set = "pub")]
name: Option<String>,
containers: Ring<Container>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
monocle_container: Option<Container>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get_copy = "pub", set = "pub")]
monocle_container_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
maximized_window: Option<Window>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get_copy = "pub", set = "pub")]
maximized_window_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub")]
floating_windows: Vec<Window>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
layout: Layout,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
layout_rules: Vec<(usize, Layout)>,
#[getset(get_copy = "pub", set = "pub")]
layout_flip: Option<Axis>,
#[getset(get_copy = "pub", set = "pub")]
workspace_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
container_padding: Option<i32>,
#[serde(skip_serializing)]
#[serde(skip)]
#[getset(get = "pub", set = "pub")]
latest_layout: Vec<Rect>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
@@ -68,6 +74,7 @@ impl Default for Workspace {
monocle_container_restore_idx: None,
floating_windows: Vec::default(),
layout: Layout::Default(DefaultLayout::BSP),
layout_rules: vec![],
layout_flip: None,
workspace_padding: Option::from(10),
container_padding: Option::from(10),
@@ -163,6 +170,20 @@ impl Workspace {
self.enforce_resize_constraints();
if !self.layout_rules().is_empty() {
let mut updated_layout = None;
for rule in self.layout_rules() {
if self.containers().len() >= rule.0 {
updated_layout = Option::from(rule.1.clone());
}
}
if let Some(updated_layout) = updated_layout {
self.set_layout(updated_layout);
}
}
if *self.tile() {
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window_mut() {
@@ -316,6 +337,28 @@ impl Workspace {
None
}
pub fn contains_managed_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {
return true;
}
}
if let Some(window) = self.maximized_window() {
if hwnd == window.hwnd {
return true;
}
}
if let Some(container) = self.monocle_container() {
if container.contains_window(hwnd) {
return true;
}
}
false
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {

View File

@@ -96,6 +96,10 @@ SendToWorkspace(target) {
Run, komorebic.exe send-to-workspace %target%, , Hide
}
SendToMonitorWorkspace(target_monitor, target_workspace) {
Run, komorebic.exe send-to-monitor-workspace %target_monitor% %target_workspace%, , Hide
}
FocusMonitor(target) {
Run, komorebic.exe focus-monitor %target%, , Hide
}
@@ -184,6 +188,18 @@ WorkspaceCustomLayout(monitor, workspace, path) {
Run, komorebic.exe workspace-custom-layout %monitor% %workspace% %path%, , Hide
}
WorkspaceLayoutRule(monitor, workspace, at_container_count, layout) {
Run, komorebic.exe workspace-layout-rule %monitor% %workspace% %at_container_count% %layout%, , Hide
}
WorkspaceCustomLayoutRule(monitor, workspace, at_container_count, path) {
Run, komorebic.exe workspace-custom-layout-rule %monitor% %workspace% %at_container_count% %path%, , Hide
}
ClearWorkspaceLayoutRules(monitor, workspace) {
Run, komorebic.exe clear-workspace-layout-rules %monitor% %workspace%, , Hide
}
WorkspaceTiling(monitor, workspace, value) {
Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide
}
@@ -240,24 +256,36 @@ WindowHidingBehaviour(hiding_behaviour) {
Run, komorebic.exe window-hiding-behaviour %hiding_behaviour%, , Hide
}
UnmanagedWindowOperationBehaviour(operation_behaviour) {
Run, komorebic.exe unmanaged-window-operation-behaviour %operation_behaviour%, , Hide
}
FloatRule(identifier, id) {
Run, komorebic.exe float-rule %identifier% %id%, , Hide
Run, komorebic.exe float-rule %identifier% "%id%", , Hide
}
ManageRule(identifier, id) {
Run, komorebic.exe manage-rule %identifier% %id%, , Hide
Run, komorebic.exe manage-rule %identifier% "%id%", , Hide
}
WorkspaceRule(identifier, id, monitor, workspace) {
Run, komorebic.exe workspace-rule %identifier% %id% %monitor% %workspace%, , Hide
Run, komorebic.exe workspace-rule %identifier% "%id%" %monitor% %workspace%, , Hide
}
IdentifyObjectNameChangeApplication(identifier, id) {
Run, komorebic.exe identify-object-name-change-application %identifier% "%id%", , Hide
}
IdentifyTrayApplication(identifier, id) {
Run, komorebic.exe identify-tray-application %identifier% %id%, , Hide
Run, komorebic.exe identify-tray-application %identifier% "%id%", , Hide
}
IdentifyBorderOverflow(identifier, id) {
Run, komorebic.exe identify-border-overflow %identifier% %id%, , Hide
IdentifyLayeredApplication(identifier, id) {
Run, komorebic.exe identify-layered-application %identifier% "%id%", , Hide
}
IdentifyBorderOverflowApplication(identifier, id) {
Run, komorebic.exe identify-border-overflow-application %identifier% "%id%", , Hide
}
FocusFollowsMouse(boolean_state, implementation) {
@@ -278,4 +306,16 @@ ToggleMouseFollowsFocus() {
AhkLibrary() {
Run, komorebic.exe ahk-library, , Hide
}
AhkAppSpecificConfiguration(path, override_path) {
Run, komorebic.exe ahk-app-specific-configuration %path% %override_path%, , Hide
}
FormatAppSpecificConfiguration(path) {
Run, komorebic.exe format-app-specific-configuration %path%, , Hide
}
NotificationSchema() {
Run, komorebic.exe notification-schema, , Hide
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.7"
version = "0.1.8"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]
@@ -15,18 +15,20 @@ derive-ahk = { path = "../derive-ahk" }
komorebi-core = { path = "../komorebi-core" }
clap = { version = "3", features = ["derive", "wrap_help"] }
color-eyre = "0.5"
color-eyre = "0.6"
dirs = "4"
fs-tail = "0.1"
heck = "0.4"
lazy_static = "1"
paste = "1"
powershell_script = "0.2"
powershell_script = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.8"
uds_windows = "1"
[dependencies.windows]
version = "0.30"
version = "0.35"
features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging"

View File

@@ -1,6 +1,7 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::fs;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
@@ -17,6 +18,7 @@ use color_eyre::eyre::anyhow;
use color_eyre::Result;
use fs_tail::TailedFile;
use heck::ToKebabCase;
use lazy_static::lazy_static;
use paste::paste;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
@@ -27,18 +29,39 @@ use windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use derive_ahk::AhkFunction;
use derive_ahk::AhkLibrary;
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
use komorebi_core::OperationBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;
lazy_static! {
static ref HOME_DIR: PathBuf = {
if let Ok(home_path) = std::env::var("KOMOREBI_CONFIG_HOME") {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
}
} else {
dirs::home_dir().expect("there is no home directory")
}
};
}
trait AhkLibrary {
fn generate_ahk_library() -> String;
}
@@ -92,6 +115,7 @@ gen_enum_subcommand_args! {
MouseFollowsFocus: BooleanState,
Query: StateQuery,
WindowHidingBehaviour: HidingBehaviour,
UnmanagedWindowOperationBehaviour: OperationBehaviour,
}
macro_rules! gen_target_subcommand_args {
@@ -151,6 +175,15 @@ gen_workspace_subcommand_args! {
Tiling: #[enum] BooleanState,
}
#[derive(Parser, AhkFunction)]
pub struct ClearWorkspaceLayoutRules {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
}
#[derive(Parser, AhkFunction)]
pub struct WorkspaceCustomLayout {
/// Monitor index (zero-indexed)
@@ -163,6 +196,35 @@ pub struct WorkspaceCustomLayout {
path: String,
}
#[derive(Parser, AhkFunction)]
pub struct WorkspaceLayoutRule {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
/// The number of window containers on-screen required to trigger this layout rule
at_container_count: usize,
layout: DefaultLayout,
}
#[derive(Parser, AhkFunction)]
pub struct WorkspaceCustomLayoutRule {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
/// The number of window containers on-screen required to trigger this layout rule
at_container_count: usize,
/// JSON or YAML file from which the custom layout definition should be loaded
path: String,
}
#[derive(Parser, AhkFunction)]
struct Resize {
#[clap(arg_enum)]
@@ -225,6 +287,14 @@ struct FocusMonitorWorkspace {
target_workspace: usize,
}
#[derive(Parser, AhkFunction)]
pub struct SendToMonitorWorkspace {
/// Target monitor index (zero-indexed)
target_monitor: usize,
/// Workspace index on the target monitor (zero-indexed)
target_workspace: usize,
}
macro_rules! gen_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ $(,)? ) => {
@@ -286,7 +356,9 @@ gen_application_target_subcommand_args! {
FloatRule,
ManageRule,
IdentifyTrayApplication,
IdentifyBorderOverflow,
IdentifyLayeredApplication,
IdentifyObjectNameChangeApplication,
IdentifyBorderOverflowApplication,
}
#[derive(Parser, AhkFunction)]
@@ -352,6 +424,20 @@ struct Unsubscribe {
named_pipe: String,
}
#[derive(Parser, AhkFunction)]
struct AhkAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
path: String,
/// Optional YAML file of overrides to apply over the first file
override_path: Option<String>,
}
#[derive(Parser, AhkFunction)]
struct FormatAppSpecificConfiguration {
/// YAML file from which the application-specific configurations should be loaded
path: String,
}
#[derive(Parser)]
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
struct Opts {
@@ -368,13 +454,13 @@ enum SubCommand {
/// Show a JSON representation of the current window manager state
State,
/// Query the current window manager state
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
Query(Query),
/// Subscribe to komorebi events
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
Subscribe(Subscribe),
/// Unsubscribe from komorebi events
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
Unsubscribe(Unsubscribe),
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
Log,
@@ -385,120 +471,132 @@ enum SubCommand {
#[clap(alias = "quick-load")]
QuickLoadResize,
/// Save the current resize layout dimensions to a file
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
#[clap(alias = "save")]
SaveResize(SaveResize),
/// Load the resize layout dimensions from a file
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
#[clap(alias = "load")]
LoadResize(LoadResize),
/// Change focus to the window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
Focus(Focus),
/// Move the focused window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
Move(Move),
/// Change focus to the window in the specified cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
CycleFocus(CycleFocus),
/// Move the focused window in the specified cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
CycleMove(CycleMove),
/// Stack the focused window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
Stack(Stack),
/// Resize the focused window in the specified direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
#[clap(alias = "resize")]
ResizeEdge(Resize),
/// Resize the focused window or primary column along the specified axis
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
ResizeAxis(ResizeAxis),
/// Unstack the focused window
Unstack,
/// Cycle the focused stack in the specified cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
CycleStack(CycleStack),
/// Move the focused window to the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
MoveToMonitor(MoveToMonitor),
/// Move the focused window to the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
MoveToWorkspace(MoveToWorkspace),
/// Send the focused window to the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
SendToMonitor(SendToMonitor),
/// Send the focused window to the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
SendToWorkspace(SendToWorkspace),
/// Send the focused window to the specified monitor workspace
#[clap(arg_required_else_help = true)]
SendToMonitorWorkspace(SendToMonitorWorkspace),
/// Focus the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
FocusMonitor(FocusMonitor),
/// Focus the specified workspace on the focused monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
FocusWorkspace(FocusWorkspace),
/// Focus the specified workspace on the target monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
FocusMonitorWorkspace(FocusMonitorWorkspace),
/// Focus the monitor in the given cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
CycleMonitor(CycleMonitor),
/// Focus the workspace in the given cycle direction
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
CycleWorkspace(CycleWorkspace),
/// Move the focused workspace to the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
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)]
#[clap(arg_required_else_help = true)]
ResizeDelta(ResizeDelta),
/// Set the invisible border dimensions around each window
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
InvisibleBorders(InvisibleBorders),
/// Set offsets to exclude parts of the work area from tiling
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WorkAreaOffset(WorkAreaOffset),
/// Adjust container padding on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
AdjustContainerPadding(AdjustContainerPadding),
/// Adjust workspace padding on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
AdjustWorkspacePadding(AdjustWorkspacePadding),
/// Set the layout on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
ChangeLayout(ChangeLayout),
/// Load a custom layout from file for the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
LoadCustomLayout(LoadCustomLayout),
/// Flip the layout on the focused workspace (BSP only)
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
FlipLayout(FlipLayout),
/// Promote the focused window to the top of the tree
Promote,
/// Force the retiling of all managed windows
Retile,
/// Create at least this many workspaces for the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
EnsureWorkspaces(EnsureWorkspaces),
/// Set the container padding for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
ContainerPadding(ContainerPadding),
/// Set the workspace padding for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WorkspacePadding(WorkspacePadding),
/// Set the layout for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WorkspaceLayout(WorkspaceLayout),
/// Set a custom layout for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WorkspaceCustomLayout(WorkspaceCustomLayout),
/// Add a dynamic layout rule for the specified workspace
#[clap(arg_required_else_help = true)]
WorkspaceLayoutRule(WorkspaceLayoutRule),
/// Add a dynamic custom layout for the specified workspace
#[clap(arg_required_else_help = true)]
WorkspaceCustomLayoutRule(WorkspaceCustomLayoutRule),
/// Clear all dynamic layout rules for the specified workspace
#[clap(arg_required_else_help = true)]
ClearWorkspaceLayoutRules(ClearWorkspaceLayoutRules),
/// Enable or disable window tiling for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WorkspaceTiling(WorkspaceTiling),
/// Set the workspace name for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WorkspaceName(WorkspaceName),
/// Toggle the behaviour for new windows (stacking or dynamic tiling)
ToggleWindowContainerBehaviour,
@@ -521,43 +619,63 @@ enum SubCommand {
/// Reload ~/komorebi.ahk (if it exists)
ReloadConfiguration,
/// Enable or disable watching of ~/komorebi.ahk (if it exists)
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WatchConfiguration(WatchConfiguration),
/// Set the window behaviour when switching workspaces / cycling stacks
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WindowHidingBehaviour(WindowHidingBehaviour),
/// Set the operation behaviour when the focused window is not managed
#[clap(arg_required_else_help = true)]
UnmanagedWindowOperationBehaviour(UnmanagedWindowOperationBehaviour),
/// Add a rule to always float the specified application
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
FloatRule(FloatRule),
/// Add a rule to always manage the specified application
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
ManageRule(ManageRule),
/// Add a rule to associate an application with a workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
WorkspaceRule(WorkspaceRule),
/// Identify an application that sends EVENT_OBJECT_NAMECHANGE on launch
#[clap(arg_required_else_help = true)]
IdentifyObjectNameChangeApplication(IdentifyObjectNameChangeApplication),
/// Identify an application that closes to the system tray
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
IdentifyTrayApplication(IdentifyTrayApplication),
/// Identify an application that has WS_EX_LAYERED, but should still be managed
#[clap(arg_required_else_help = true)]
IdentifyLayeredApplication(IdentifyLayeredApplication),
/// Identify an application that has overflowing borders
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
IdentifyBorderOverflow(IdentifyBorderOverflow),
#[clap(arg_required_else_help = true)]
#[clap(alias = "identify-border-overflow")]
IdentifyBorderOverflowApplication(IdentifyBorderOverflowApplication),
/// Enable or disable focus follows mouse for the operating system
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
FocusFollowsMouse(FocusFollowsMouse),
/// Toggle focus follows mouse for the operating system
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
ToggleFocusFollowsMouse(ToggleFocusFollowsMouse),
/// Enable or disable mouse follows focus on all workspaces
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(arg_required_else_help = true)]
MouseFollowsFocus(MouseFollowsFocus),
/// Toggle mouse follows focus on all workspaces
ToggleMouseFollowsFocus,
/// Generate a library of AutoHotKey helper functions
AhkLibrary,
/// Generate common app-specific configurations and fixes to use in komorebi.ahk
#[clap(arg_required_else_help = true)]
#[clap(alias = "ahk-asc")]
AhkAppSpecificConfiguration(AhkAppSpecificConfiguration),
/// Format a YAML file for use with the 'ahk-app-specific-configuration' command
#[clap(arg_required_else_help = true)]
#[clap(alias = "fmt-asc")]
FormatAppSpecificConfiguration(FormatAppSpecificConfiguration),
/// Generate a JSON Schema of subscription notifications
NotificationSchema,
}
pub fn send_message(bytes: &[u8]) -> Result<()> {
let mut socket = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut socket = HOME_DIR.clone();
socket.push("komorebi.sock");
let socket = socket.as_path();
@@ -571,8 +689,7 @@ fn main() -> Result<()> {
match opts.subcmd {
SubCommand::AhkLibrary => {
let mut library =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut library = HOME_DIR.clone();
library.push("komorebic.lib.ahk");
let mut file = OpenOptions::new()
.write(true)
@@ -580,7 +697,10 @@ fn main() -> Result<()> {
.truncate(true)
.open(library.clone())?;
file.write_all(SubCommand::generate_ahk_library().as_bytes())?;
let output: String = SubCommand::generate_ahk_library();
let fixed_output = output.replace("%id%", "\"%id%\"");
file.write_all(fixed_output.as_bytes())?;
println!(
"\nAHK helper library for komorebic written to {}",
@@ -637,6 +757,15 @@ fn main() -> Result<()> {
SubCommand::SendToWorkspace(arg) => {
send_message(&*SocketMessage::SendContainerToWorkspaceNumber(arg.target).as_bytes()?)?;
}
SubCommand::SendToMonitorWorkspace(arg) => {
send_message(
&*SocketMessage::SendContainerToMonitorWorkspaceNumber(
arg.target_monitor,
arg.target_workspace,
)
.as_bytes()?,
)?;
}
SubCommand::MoveWorkspaceToMonitor(arg) => {
send_message(&*SocketMessage::MoveWorkspaceToMonitorNumber(arg.target).as_bytes()?)?;
}
@@ -715,6 +844,34 @@ fn main() -> Result<()> {
.as_bytes()?,
)?;
}
SubCommand::WorkspaceLayoutRule(arg) => {
send_message(
&*SocketMessage::WorkspaceLayoutRule(
arg.monitor,
arg.workspace,
arg.at_container_count,
arg.layout,
)
.as_bytes()?,
)?;
}
SubCommand::WorkspaceCustomLayoutRule(arg) => {
send_message(
&*SocketMessage::WorkspaceLayoutCustomRule(
arg.monitor,
arg.workspace,
arg.at_container_count,
resolve_windows_path(&arg.path)?,
)
.as_bytes()?,
)?;
}
SubCommand::ClearWorkspaceLayoutRules(arg) => {
send_message(
&*SocketMessage::ClearWorkspaceLayoutRules(arg.monitor, arg.workspace)
.as_bytes()?,
)?;
}
SubCommand::WorkspaceTiling(arg) => {
send_message(
&*SocketMessage::WorkspaceTiling(arg.monitor, arg.workspace, arg.value.into())
@@ -846,7 +1003,7 @@ fn main() -> Result<()> {
)?;
}
SubCommand::State => {
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let home = HOME_DIR.clone();
let mut socket = home;
socket.push("komorebic.sock");
let socket = socket.as_path();
@@ -880,7 +1037,7 @@ fn main() -> Result<()> {
}
}
SubCommand::Query(arg) => {
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let home = HOME_DIR.clone();
let mut socket = home;
socket.push("komorebic.sock");
let socket = socket.as_path();
@@ -914,8 +1071,7 @@ fn main() -> Result<()> {
}
}
SubCommand::RestoreWindows => {
let mut hwnd_json =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut hwnd_json = HOME_DIR.clone();
hwnd_json.push("komorebi.hwnd.json");
let file = File::open(hwnd_json)?;
@@ -948,15 +1104,28 @@ fn main() -> Result<()> {
&*SocketMessage::WatchConfiguration(arg.boolean_state.into()).as_bytes()?,
)?;
}
SubCommand::IdentifyObjectNameChangeApplication(target) => {
send_message(
&*SocketMessage::IdentifyObjectNameChangeApplication(target.identifier, target.id)
.as_bytes()?,
)?;
}
SubCommand::IdentifyTrayApplication(target) => {
send_message(
&*SocketMessage::IdentifyTrayApplication(target.identifier, target.id)
.as_bytes()?,
)?;
}
SubCommand::IdentifyBorderOverflow(target) => {
SubCommand::IdentifyLayeredApplication(target) => {
send_message(
&*SocketMessage::IdentifyBorderOverflow(target.identifier, target.id).as_bytes()?,
&*SocketMessage::IdentifyLayeredApplication(target.identifier, target.id)
.as_bytes()?,
)?;
}
SubCommand::IdentifyBorderOverflowApplication(target) => {
send_message(
&*SocketMessage::IdentifyBorderOverflowApplication(target.identifier, target.id)
.as_bytes()?,
)?;
}
SubCommand::Manage => {
@@ -998,6 +1167,97 @@ fn main() -> Result<()> {
SubCommand::WindowHidingBehaviour(arg) => {
send_message(&*SocketMessage::WindowHidingBehaviour(arg.hiding_behaviour).as_bytes()?)?;
}
SubCommand::UnmanagedWindowOperationBehaviour(arg) => {
send_message(
&*SocketMessage::UnmanagedWindowOperationBehaviour(arg.operation_behaviour)
.as_bytes()?,
)?;
}
SubCommand::AhkAppSpecificConfiguration(arg) => {
let content = fs::read_to_string(resolve_windows_path(&arg.path)?)?;
let lines = if let Some(override_path) = arg.override_path {
let override_content = fs::read_to_string(resolve_windows_path(&override_path)?)?;
ApplicationConfigurationGenerator::generate_ahk(
&content,
Option::from(override_content.as_str()),
)?
} else {
ApplicationConfigurationGenerator::generate_ahk(&content, None)?
};
let mut generated_config = HOME_DIR.clone();
generated_config.push("komorebi.generated.ahk");
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(generated_config.clone())?;
file.write_all(lines.join("\n").as_bytes())?;
println!(
"\nApplication-specific generated configuration written to {}",
generated_config.to_str().ok_or_else(|| anyhow!(
"could not find the path to the generated configuration file"
))?
);
println!(
"\nYou can include the generated configuration at the top of your komorebi.ahk config with this line:"
);
println!("\n#Include %A_ScriptDir%\\komorebi.generated.ahk");
}
SubCommand::FormatAppSpecificConfiguration(arg) => {
let file_path = resolve_windows_path(&arg.path)?;
let content = fs::read_to_string(&file_path)?;
let formatted_content = ApplicationConfigurationGenerator::format(&content)?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?;
file.write_all(formatted_content.as_bytes())?;
println!("File successfully formatted for PRs to https://github.com/LGUG2Z/komorebi-application-specific-configuration");
}
SubCommand::NotificationSchema => {
let home = HOME_DIR.clone();
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::NotificationSchema.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);
}
}
}
}
Ok(())