Compare commits

...

96 Commits

Author SHA1 Message Date
LGUG2Z
1834f092d7 ci(github): update build + release workflow
This commit updates the build and release workflow to enable multi-arch
builds and releases.

A number of Rust-specific actions have been added, namely rust-cache to
handle cargo caching and actions-rust-cross to handle cross-compilation.

A release-dry-run target has been added to run on master which should
help catch any issues in release workflow changes early.

Releases drop goreleaser entirely in favour of action-gh-release which
was already in use to add msi installers to the releases previously
created by goreleaser.
2024-10-15 15:42:11 -07:00
Dovie Weinstock
aa5a36989f fix(cli): escape start --ahk args w/ double quotes
fix #1040
2024-10-14 17:29:33 -07:00
LGUG2Z
b612066367 feat(config): add support for v2 asc json
This commit adds support for a v2 format of the application specific
configuration file, centralizing on JSON to maximize the knowledge
crossover for people already familiar with the types used in
komorebi.json.

The biggest difference besides the format change is that matchers must
be used explicitly for every kind of rule, rather than being able to
specify options on a default rule. This is a bit more verbose, but
ultimately allows for significantly more flexibility.
2024-10-14 16:50:03 -07:00
dependabot[bot]
cbe5b24f73 chore(deps): bump egui-phosphor from 0.7.2 to 0.7.3
Bumps [egui-phosphor](https://github.com/amPerl/egui-phosphor) from 0.7.2 to 0.7.3.
- [Changelog](https://github.com/amPerl/egui-phosphor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amPerl/egui-phosphor/compare/v0.7.2...v0.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 07:21:15 -07:00
LGUG2Z
2ead216eeb chore(just): add install-targets to justfile 2024-10-13 15:07:31 -07:00
LGUG2Z
b0944662fa fix(wm): add cmd thread lock acquisition timeouts
After some investigation by @alex-ds13 on Discord it looks like there
are times where attempting to gain a lock on the WindowManager inside of
read_commands_uds results in the thread becoming blocked when it's not
possible to obtain the lock.

Instead of waiting indefinitely for a lock, this change ensures that we
will wait for at most 1 second before discarding the message so that the
command listener loop can continue.

Warning logs have been added to inform when a message has been dropped
as a result of lock acquisition failure.
2024-10-13 15:07:31 -07:00
LGUG2Z
1376d7be04 feat(wm): add retile with resize socket msg
This commit adds a new SocketMessage variant,
RetileWithResizeDimensions, to preserve any resize dimensions applied by
the user.

This new variant is now used when clicking on a workspace using the
komorebi widget in komorebi-bar.
2024-10-13 15:07:31 -07:00
LGUG2Z
5da72e10df feat(client): add subscribe_with_options
This commit adds a new method, subscribe_with_options to
komorebi-client.

The first option introduced is to tell komorebi to only send
notifications when the window manager state has been changed during the
processing of an event.

This new subscription option is now used with komorebi-bar to improve
rendering and update performance.
2024-10-13 15:07:31 -07:00
LGUG2Z
d21ffb28cc fix(cli): update fetch-asc output to use '/' 2024-10-13 15:07:31 -07:00
LGUG2Z
4e27febdc0 chore(cargo): suppress macro lint warnings 2024-10-13 15:07:31 -07:00
LGUG2Z
3c5852ae20 docs(cli): highlight eol features in start + check 2024-10-13 15:07:27 -07:00
LGUG2Z
7943fccb1b chore(cargo): +nightly fmt 2024-10-13 10:05:11 -07:00
LGUG2Z
2d2cea31c0 docs(schema): update all json schemas 2024-10-13 10:05:11 -07:00
alex-ds13
a17924c2af feat(config): add floating border colour opt 2024-10-13 10:05:11 -07:00
alex-ds13
91519227d4 fix(wm): allow cross-monitor floating window moves
This commit changes the `move_container_to_monitor` from the WM to allow
moving floating windows as well.

It also adds a new method `move_to_area` to the `Window` that allows
moving a window from one monitor to another keeping its size.
2024-10-13 10:05:11 -07:00
alex-ds13
f91d0aabf5 fix(wm): check exhaustively for ws emptiness
This commit creates a new function for the workspaces to check if they
are empty or not.

This function properly accounts for maximized windows, monocle windows
and floating windows.

This should fix the cases where the WM was checking if the workspace was
empty to focus the desktop in order to loose focus from previously
focused window.

Previously it wasn't checking for floating windows so it cause continues
focus flickering when there were only floating windows on the workspace.
2024-10-13 10:05:11 -07:00
alex-ds13
d5b6584042 feat(wm): add float override option
This commit introduces a new option `float_override`, which makes it so
every every window opened, shown or uncloaked will be set to floating,
but it won't be ignored. It will be added to the floating_windows of the
workspace, meaning that the user can later tile that window with
toggle-float command.

This allows the users to have all windows open as floating and then
manually tile the ones they want.

This interactively rebased commit contains changes from the following
individual commits:

0e8dc85fb1
feat(wm): add new float override option

30bdaf33d5
feat(cli): add command for new option `ToggleFloatOverride`

b7bedce1ca
feat(wm): add window_container_behaviour and float_override to workspaces

221e4ea545
feat(cli): add commands for workspace new window behaviour and float_override

b182cb5818
fix(wm): show floating apps in front of stacked windows as well

7c9cb11a9b
fix(wm): Remove unecessary duplicated code
2024-10-13 10:05:11 -07:00
LGUG2Z
6db317d425 feat(wm): separate floating and ignored apps
This commit introduces a distinction between ignored applications
(previously identified with float_rules) and floating applications.

All instances of "float_" with the initial meaning of "ignored" have
been renamed with backwards compatibility aliases.

Floating applications will be managed under Workspace.floating_windows
if identified using a rule, and this allows them to now be moved across
workspaces.

A new border type has been added for floating applications, and the
colour can be configured via theme.floating_border.

This interactively rebased commit contains changes from the following
individual commits:

17ea1e6869
feat(wm): separate floating and ignored apps

8b344496e6
feat(wm): allow ws moves of floating apps

7d8e2ad814
refactor(wm): float_rules > ignore_rules w/ compat

d68346a640
fix(borders): no redraws on floating win title change

a93e937772
fix(borders): update on floating win drag

68e9365dda
fix(borders): send notif on ignored hwnd events
2024-10-13 10:05:11 -07:00
Csaba
07a1538905 feat(bar): add label prefix config opt
This commit makes the label prefix configurable. Users can select if
they want to show an icon, only text, or both text and an icon.
2024-10-13 10:05:11 -07:00
LGUG2Z
853db2f15f docs(github): update issue templates 2024-10-13 10:05:11 -07:00
LGUG2Z
c9f180ce0f chore(deps): cargo update 2024-10-13 10:05:11 -07:00
LGUG2Z
bc00f54c90 feat(bar): add more logging around error paths 2024-10-13 10:05:11 -07:00
LGUG2Z
c57759242a feat(wm): delete stale sub socket files 2024-10-13 10:05:11 -07:00
LGUG2Z
6f27de8e5c chore(deps): bump eframe from 0.28 to 0.29 2024-10-13 10:05:11 -07:00
Csaba
a9e98034b0 feat(bar): add cpu widget
This commit adds a CPU widget, following the patterns of the Memory
widget.
2024-10-13 10:05:11 -07:00
LGUG2Z
3489163793 refactor(wm): standardize config env var handling
This commit ensures that whenever komorebi.json is read and deserialized
into StaticConfig via StaticConfig::read, all known paths where
$Env:KOMOREBI_CONFIG_HOME and $Env:USERPROFILE are accepted will be run
through the resolve_home_path helper fn.
2024-10-13 10:05:11 -07:00
LGUG2Z
71d65cf4a1 fix(wm): ignore minimize calls on komorebi-bar
Hopefully I don't have to make this yet another configurable list...
2024-10-13 10:05:10 -07:00
alex-ds13
a5e6828d1e fix(wm): update monitor focus before focus-stack-window
This commit fixes the cases where you'd call this command on a monitor
which was not focused, for example by pressing a button on a bar like
komorebi-bar or other when you had focus on another monitor.
This change ensures that first we focus the monitor where the mouse cursor
is, this way it will act on the monitor that you've just pressed instead
of the monior that was focused before.
2024-10-13 10:05:10 -07:00
LGUG2Z
96605f72a7 feat(config): add bar configurations opt
This commit adds a "bar_configurations" option to the static config file
which takes an array of PathBufs.

If this option is defined and the --bar flag is passed to the "komorebic
start" command, komorebic will attempt to launch multiple instances of
komorebi-bar.exe with the --config flag pointing to the PathBufs given.

This configuration option is only consumed by komorebic, not by the
window manager directly, so it could also be used by other status bar
projects to read configuration file locations from.

There is no requirement for the PathBufs to point specifically to
komorebi bar configuration files if the --bar flag is not being used
with "komorebic start".
2024-10-13 10:05:10 -07:00
LGUG2Z
18bb060b71 refactor(bar): use native apis for positioning
This commit replaces almost all uses of the egui Viewport API for bar
window positioning with calls to SetWindowPos via komorebi_client's
Window struct.

This seems to play much more smoothly with multi-monitor setups where
each monitor has a different scaling factor, opening the door for
multiple instances of komorebi-bar.exe to run against multiple monitors.

As a result of this change, the "viewport" configuration option has been
renamed to "position" and doc strings have been changed to remove the
reference to the egui crate docs. Similarly, "viewport.position" and
"viewport.inner_size" have been renamed to "position.start" and
"position.end" respectively. Backwards-compatibility aliases have been
included for all renames.
2024-10-13 10:05:10 -07:00
LGUG2Z
20f370a51d feat(bar): dpi-awareness + position hotloading
This commit ensures that the komorebi-bar.exe process is DPI aware and
applies DPI compensation to viewport.position and viewport.inner size
both on launch and on configuration reload.  viewport.position changes
are now hotloaded wihtout having to restart the process.

re #1024
2024-10-04 16:09:14 -07:00
LGUG2Z
2d1613b4d9 feat(bar): use unique subscriber names
This commit ensures that multiple processes of komorebi-bar.exe use
unique, randomly-suffixed subscriber names when registering with
komorebi.
2024-10-04 15:22:40 -07:00
LGUG2Z
216154b975 fix(border): remove windows accent from monocle on restore 2024-10-04 09:51:18 -07:00
LGUG2Z
fe9b7e53b7 fix(wm): always focus desktop on empty ws
This commit ensures that when switching to a workspace, if that
workspace is empty, the desktop window will be focused (and focus will
be removed from the last focused window on the previous workspace)
regardless of the value of follow_focus.
2024-10-04 09:51:13 -07:00
LGUG2Z
d9bffa06df feat(wm): configurable slow app compensation time
This commit ensures that other "slow" applications (besides firefox)
which require a compensation time to sleep before continuing with
eligibility checks can be configured by end users in komorebi.json.

As the compensation time will vary depending on the specs of the end
user's machine, the compensation time in ms has also been made
configurable.
2024-10-02 13:03:10 -07:00
LGUG2Z
d34363af8f docs(readme): update quickstart video 2024-10-02 11:41:51 -07:00
LGUG2Z
db4b0dd63b fix(windows): conditional compilation for 32-bit
This commit ensures that komorebi will compile when targeting 32-bit
architectures, namely `stable-i686-pc-windows-msvc`.

Thanks to @kennykerr for pointing out that Get/SetWindowLongPtrA/W calls
don't actually exist on 32-bit builds of Windows and are aliased instead
to Get/SetWindowLongA/W which take i32 args instead of isize args:
https://github.com/microsoft/windows-rs/issues/3304
2024-10-02 11:41:51 -07:00
LGUG2Z
7b563aac5e feat(wm): add all matching strats for ws rules
This commit ensures that the full range of matching strategies for both
Simple and Composite matching rules will be respected for both initial
and persistent workspace rules.

The generate-static-config command will no longer attempt to populate
workspace rules, and will likely slowly be deprecated as the
overwhelming majority have users have already migrated to the static
configuration file format.

fix #991
2024-10-02 11:41:13 -07:00
LGUG2Z
b7198242ff feat(transparency): add ignore rules to config
This commit adds transparency_ignore_rules to the komorebi.json static
configuration format. Windows that match any rules given in this list
will not have transparency applied when they are unfocused.

For example, to ensure that a browser window is not made transparent
when the focused tab is a YouTube video:

```json
"transparency_ignore_rules": [
  {
    "kind": "Title",
    "id": "YouTube",
    "matching_strategy": "Contains"
  }
]
```
2024-09-28 12:31:50 -07:00
LGUG2Z
a1f1be0afe chore(dev): begin v0.1.30-dev 2024-09-28 11:50:25 -07:00
LGUG2Z
e4b7adeb0f docs(mkdocs): add bar reference link to nav 2024-09-27 22:18:44 -07:00
LGUG2Z
818ac3404c chore(release): v0.1.29 2024-09-27 12:25:40 -07:00
LGUG2Z
d6ae81af13 docs(mkdocs): generate latest cli docs 2024-09-26 20:22:37 -07:00
LGUG2Z
109227b74c feat(bar): add quickstart flag, remove yaml format
This commit adds a --quickstart flag to the komorebi-bar binary to
output an example komorebi.bar.json into the user's desired
configuration directory.

This is to avoid the case where running komorebic quickstart would
result in clobbering an existing komorebi.json file.

Additionally, if a user tries to run the bar for the first time without
a configuration file, the example configuration will be written to disk
for them.

Finally support for loading a komorebi.bar.yaml file has been removed
because I have no desire to support multiple configuration formats over
the long term.
2024-09-26 18:01:57 -07:00
LGUG2Z
ddb600f745 chore(deps): bump windows-rs from 0.57 to 0.58
Not a huge fan of these updates in the windows-rs crate which swap the
isize values which were previously wrapped in various handles with *mut
core::ffi:c_void pointers.

In order to at least keep this codebase sane, all of the wrapper
functions exposed in WindowsApi now take isize wherever they previously
took HWND, HMONITOR, HINSTANCE etc.

Going forward any pub fn in WindowsApi should prefer isize over Windows
handles which wrap c_void pointers.
2024-09-25 18:25:03 -07:00
LGUG2Z
167ec92811 chore(deps): bump windows-rs from 0.54 to 0.57
This is the highest we can go right now because this change which went
into 0.58 absolutely fucks our shit up:
https://github.com/microsoft/windows-rs/pull/3111
2024-09-25 08:58:13 -07:00
LGUG2Z
d110e12a62 docs(privacy): add privacy policy 2024-09-24 15:39:22 -07:00
LGUG2Z
21bd09e419 fix(wm): add layout edge index awareness
This commit builds on c3f135703e and
1080159e68 to add layout-specific edge
index awareness to all default layouts.

This is most useful for UltrawideVerticalStack and
RightMostVerticalStack, which have a lot of edge cases due to the
positioning of the 0th index changing with the number of containers on a
workspace.
2024-09-24 15:39:19 -07:00
LGUG2Z
01ccf70ad5 feat(bar): expand komorebi layout subwidget
This commit expands the komorebi layout subwidget to update the layout
indicator accordingly when the user switches to a floating workspace, or
when the window manager is paused.
2024-09-22 18:29:07 -07:00
LGUG2Z
c3f135703e fix(wm): cross-border focus direction awareness
This commit ensures that when focusing across a monitor boundary to the
left, the container at the back of the Ring<Container> in the target
workspace will be focused, and when focusing across a monitor boundary
to the right, the one at the front will be focused.
2024-09-22 18:29:07 -07:00
LGUG2Z
3720ce42d0 fix(bar): use truncated labels for titles
This commit introduces a new wrapper, CustomUi, which is used to
implement custom methods on top of eframe::egui::Ui.

The default ui::add_sized method always has the text in a label
centered, which is not desirable for a status bar where the layout
should be ltr.

A new function CustomUi::add_sized_left_to_right has been added to
ensure that labels can be truncated with a custom width (which requires
allocate_ui_with_layout), while also retaining the ability for the text
to be aligned to the left rather than the center of the allocated
layout.
2024-09-22 18:29:03 -07:00
LGUG2Z
1080159e68 fix(wm): cross-border move direction awareness
This commit ensures that when moving across a monitor boundary to the
left, a container will be added to the back of the Ring<Container> of
the target workspace, and when moving across a monitor boundary to the
right, that it will be added to the front.
2024-09-20 17:11:19 -07:00
LGUG2Z
360d0915a1 refactor(deps): unify versions across workspace pkgs 2024-09-19 21:33:03 -07:00
LGUG2Z
182c1e6a96 feat(bar): expand focused window subwidget
This commit ensures that the focused window komorebi subwidget is aware
of multi-window containers and displays an ordered list of windows in a
container stack which can be clicked to change focus in the stack.

When there are >1 windows in a stack, the title of the focused window
will take from komorebi.json's theme.stack_border if theme is defined
(falling back to the same default value in komorebi), or from
theme.accent in komorebi.bar.json, if defined.
2024-09-19 17:00:13 -07:00
LGUG2Z
14d2ebd756 feat(bar): add font size config opt 2024-09-18 18:06:45 -07:00
LGUG2Z
50b89cc1df chore(deps): cargo update 2024-09-18 15:18:32 -07:00
LGUG2Z
df409902bb refactor(bar): change panics to error logs
This commit goes through all komorebi_client calls which return a
Result<T> and replaces unwraps with error logs to avoid runtime panics.
2024-09-18 14:50:23 -07:00
LGUG2Z
df19d06333 feat(cli): generate json schemas locally
This commit updates the various komorebic json schema generation
commands to generate the schemas locally, without requiring a running
instance of komorebi to communicate with over IPC.
2024-09-18 10:59:25 -07:00
LGUG2Z
96d094d9d7 feat(cli): update enable-autostart cmd for bar 2024-09-18 10:49:00 -07:00
LGUG2Z
2916256e79 feat(wm): add replace configuration socket message
This commit introduces a new SocketMessage, ReplaceConfiguration, which
attempts to replace a running instance of WindowManager with another
created from a (presumably) different komorebi.json file.

This will likely be useful for people who have multiple different
monitor setups that they connect and disconnect from throughout the day,
but definitely needs more testing.

An experimental sub-widget which calls this SocketMessage has been added
to komorebi-bar to aid with initial testing.
2024-09-18 10:48:55 -07:00
LGUG2Z
21a2138330 docs(mkdocs): add komorebi-bar to getting started
This commit adds instructions for komorebi-bar to the Getting Started
section of the documentation website, adds an example komorebi.bar.json
configuration file, integrates that file with the quickstart command,
and updates the start and stop commands to take --bar flags similar to
the --whkd flags.

In updating the documentation I also decided to rename in the internal
tag for the KomobarTheme and KomorebiTheme enums to "palette" instead of
the previous generic "type" tag which was copied from the serde docs.
2024-09-17 18:56:12 -07:00
LGUG2Z
6addfed1ce fix(bar): read mouse follows focus from state 2024-09-17 18:09:00 -07:00
LGUG2Z
6ba0ba79f9 docs(license): fork polyform strict to explicitly allow changes 2024-09-17 17:55:44 -07:00
LGUG2Z
254fcc988f fix(bar): use custom windows-icons w/o panics
This commit uses a custom fork of windows-icons which removes runtime
panics and instead exposes a safe Option<T> based API.
2024-09-17 17:14:53 -07:00
LGUG2Z
7005a01d9c fix(wm): hot reload cross boundary behaviour changes 2024-09-16 08:13:25 -07:00
LGUG2Z
de3d4d0d99 feat(bar): hotloading for viewport inner_size
This commit adds hot reloading for changes made to viewport.inner_size
in the configuration file. I still don't understand how the scaling
works with egui, but at least for the time being there are some rough
heuristics I've thrown together.

The transformation of y still seems a little off, but the transformation
of x seems pretty accurate when dividing by native_pixels_per_point.
2024-09-15 13:27:50 -07:00
LGUG2Z
b69db863f1 feat(themes): update bar on komorebi.json reload
This commit ensures that whenever komorebi.json is updated, komorebi-bar
will try to apply whichever theme is set in that file by the user (if
one is set).

Similarly, if a theme is not set in komorebi.bar.json, komorebi-bar will
load the theme set in komorebi.json (if one is set).

A new configuration "bar_accent" has been added to the KomorebiTheme
struct to make this process as uniform as possible.
2024-09-15 10:34:37 -07:00
LGUG2Z
286bb0070c fix(bar): fmt battery percentage without decimals 2024-09-14 21:46:00 -07:00
LGUG2Z
45894be4ff feat(themes): add + integrate komorebi-themes lib
This commit abstracts the theme-related code from komorebi-bar into a
separate library which is now also consumed by komorebi.

The static configuration file for komorebi has been updated to allow
users to specify themes and provide palette overrides for various border
styles and stackbar configuration options.

In the event that both a theme and border-specific and/or
stackbar-specific colours have been specified, the theme will take
priority to make it as easy as possible for users who have already spent
time tweaking their colours to try out themes and quickly revert back if
they prefer their existing colours.

This change makes it easier for users to have unified themes between
komorebi and the komorebi status bar.
2024-09-14 16:31:39 -07:00
LGUG2Z
bc67936dd3 feat(bar): add komorebi-bar
This commit adds an initial version of the komorebi status bar.

At this point the bar is still considered "alpha" and the configuration
format may see some small breaking changes based on usage and feedback.

There is an accompanying video series which details the creation of this
bar on YouTube: https://www.youtube.com/watch?v=x2Z5-K05bHs

Some high level notes on the bar:

* An external application which exclusively consumes komorebi_client's
  public API surface - anyone can create this without hacking directly
  on komorebi's internals
* Generally a very simple bar with limited configuration options - users
  who want more configurability should use alternatives such as yasb or
  zebar
* Scope is deliberately limited to provide a tighter, more focused
  experience: Windows-only, komorebi-only, single-monitor-only,
  horizontal-only
* No support for custom widgets or templating
* Colours are controlled exclusively through themes which adhere to a
  palette framework such as Base16 or Catppuccin (and possibly others in
  the future)

This commit contains all of the commits listed:

e5fa03c33c
feat(bar): initial commit

b3990590f3
feat(bar): add config struct with basic opts

bc2f4a172e
feat(bar): handle komorebi restarts gracefully

ca6bf69ac7
feat(bar): add basic widget config opts

18358efed8
feat(bar): add interactive layout and media widgets

92bb9f680b
perf(bar): use explicit redraw and data refresh strategies

8e74e97706
feat(bar): add battery and network widgets

fdc7706d23
feat(bar): add custom font loader

025162769b
feat(bar): allow right side widget ordering

a1688691cf
feat(bar): add app icon next to focused window title

9f78739c3f
feat(bar): add komorebi widget (+config) and themes

a4ef85859e
feat(bar): use phosphor icons for uniformity

e99138a97e
feat(bar): add first pass at configuration loader

d6ccf4cf9a
feat(bar): add logging and config hotwatch

34d2431947
feat(bar): handle monocle containers in komorebi widget

7907dfeb79
feat(bar): add optional data refresh intervals to config

96a9cb320e
feat(bar): add flag to list system fonts

42b7a13693
feat(bar): add activity to network widget

ac38f52407
feat(bar): to_pretty_bytes on network activity

6803ffd741
feat(bar): configurable network activity fill char len

7d7a5d758d99808cd2b81da2b3ddbb11c52aa92f
ci(github): add bar to wix and goreleaser configs

da307e36fc1faf84ecca3f91811fdd15f70ef2ff
feat(bar): expand theme sources

c580ff7899889309dfa849ad4fb05b80b6af8d9b
feat(bar): add accent config for themes

bc4dabda4a941c0c9764fae2c8d11abbfdc0a9f5
feat(bar): add accents to widget emojis

a574837529dd6c5add73edf394c1c9c2e6cc6315
feat(bar): add to hard-coded float identifiers

ff41b552613f911e56b1790e68389525ee7e603c
chore(deps): bump base16-egui-themes
2024-09-14 14:19:15 -07:00
LGUG2Z
c06d9afa3b fix(wm): grow monitors vec to accomodate idx prefs
This commit fixes a bug in load_monitor_information which resulted in an
infinite while loop due to a misunderstanding of how VecDeque::reserve
works.
2024-08-27 16:21:59 -07:00
LGUG2Z
b799fd3077 feat(wm): add cross boundary behaviour options
This commit introduces a new configuration option,
cross_boundary_behaviour, which allows the user to decide if they want
Focus and Move operations to operate across Workspace or Monitor
boundaries.

The default behaviour in komorebi has always been Monitor.  Setting this
to Workspace will make komorebi act a little like PaperWM, where
"komorebic focus left" and "komorebic focus right" will switch to the
next or previous workspace respectively if the currently focused window
as at either the left or right monitor boundary.

resolve #959
2024-08-26 21:49:08 -07:00
thearturca
3c03528750 fix(animation): enable cross-monitor animations
This commit is a squashed combination of the following commits from #920
by @thearturca. Thanks to both @thearturca for @amnweb for their work in
fixing and thoroughly testing these changes respectively.

935079281a
fix(animation): added pending cancel count to track `is_cancelled` state

84ad947e1f
refactor(animation): remove cancel idx decreasing

804b0380f7
refactor(animation): remove `ANIMATION_TEMPORARILY_DISABLED` global vars

f25787393c
fix(animation): extend cancelling system to support multiple cancel call

dfd6e98e9c
refactor(window): reuse window rect in `animate_position` method

18522db902
fix(animations): change check for existings animation to `pending_cancel_count` field

Before it was checking `cancel_idx_counter` which is `id` counter. It
never gonna equals `0` and doesn't represent all animations that running
for that window. So it doesn't delete entry from hashmap.
That leads to bug when border and stackbar doesn't get notified after
animation ends.
2024-08-25 13:44:50 -07:00
LGUG2Z
821a124771 fix(wm): socket cleanup on exit
This commit ensures that Shutdown signals will be sent to subscriber
sockets and that "komorebi.sock" will be cleaned up on exit.

Alongside these changes, komorebi_client::send_message no longer retries
so that integrators can receive feedback via io::Result errors when
komorebi is not running.
2024-08-12 19:07:33 -07:00
LGUG2Z
8f7b9202b2 refactor(wm): reduce process_event log noise 2024-08-06 17:12:36 -07:00
LGUG2Z
13e2cbc7a1 fix(wm): exclude minimized hwnds from show event
This commit ensures that a WindowManagerEvent::Show will not be
triggered when a WinEvent::ObjectNameChange is received for an
application in the object_name_change_on_launch whitelist.

This notably impacts Firefox when the window title changes while the
application is minimized (for example, on a page with YouTube autoplay
enabled).

fix #941
2024-08-06 16:41:52 -07:00
LGUG2Z
ff653e78af fix(wm): mouse resize on right and bottom edges
Addresses a regression introduced somewhere along the way in changing
how borders and rects sizes are calculated. Need to come back and see if
the constant calculated with the mix of BORDER_WIDTH and BORDER_OFFSET
is still relevant anymore.

fix #942
2024-08-06 13:59:06 -07:00
npc203
6d038b8b18 fix(cli): correct cycle-layout prev/next seq 2024-08-06 13:03:10 -07:00
dependabot[bot]
45a5941872 chore(deps): bump regex from 1.10.5 to 1.10.6
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.5 to 1.10.6.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.5...1.10.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 13:16:17 -07:00
dependabot[bot]
f54097f094 chore(deps): bump clap from 4.5.9 to 4.5.13
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.9 to 4.5.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/clap_complete-v4.5.9...v4.5.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 13:16:03 -07:00
dependabot[bot]
29b14f8dc8 chore(deps): bump dunce from 1.0.4 to 1.0.5
Bumps [dunce](https://gitlab.com/kornelski/dunce) from 1.0.4 to 1.0.5.
- [Commits](https://gitlab.com/kornelski/dunce/compare/v1.0.4...v1.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 13:15:51 -07:00
dependabot[bot]
a1cf5ba29c chore(deps): bump which from 6.0.1 to 6.0.2
Bumps [which](https://github.com/harryfei/which-rs) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/harryfei/which-rs/releases)
- [Changelog](https://github.com/harryfei/which-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harryfei/which-rs/compare/6.0.1...6.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 13:15:43 -07:00
dependabot[bot]
a60e5a77c2 chore(deps): bump serde_json from 1.0.120 to 1.0.122
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.120 to 1.0.122.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.120...v1.0.122)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 13:15:32 -07:00
LGUG2Z
f722905be1 feat(cli): add focus-stack-window cmd
This commit adds a new command, focus-stack-window, which allows users
to focus windows in the focused container stack by their index
(zero-indexed) within the stack.

If the user tries to focus an index which does not correspond to a
window within the container stack, an error will be logged.
2024-08-02 12:26:05 -07:00
LGUG2Z
b5eafc6b96 fix(borders): maximize compat w/ komorebi impl
This commit ensures that the "Komorebi" border implementation is set as
the default as it has the maximum range of compat across different
Windows versions, whereas the "Windows" implementation requires Win 11.

Because "Windows" implementation methods will error on Windows 10,
restore_all_windows has been updated to only attempt to remove accents
if BorderImplementation::Windows is selected (this is gated behind the
WINDOWS_11 check).

re #925
2024-07-26 16:00:17 -07:00
LGUG2Z
c367967301 fix(wm): apply window based offsets to monocles
This commit ensures that when a window_based_work_area_offset is set,
and the window limit is greater than 0, the offset will be applied to
monocle containers on a workspace (unless an override is specified for
that workspace).
2024-07-24 10:46:57 -07:00
LGUG2Z
780635c8ef fix(transparency): handle multi-monitor monocles
This commit ensures that transparency will be set accordingly when focus
moves to and from monocle containers on multi-monitor setups.
2024-07-24 10:38:10 -07:00
dependabot[bot]
d5bec7afa4 chore(deps): bump openssl from 0.10.64 to 0.10.66
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.64 to 0.10.66.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.64...openssl-v0.10.66)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 11:18:39 -07:00
dependabot[bot]
974aa0d000 chore(deps): bump thiserror from 1.0.62 to 1.0.63
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.62 to 1.0.63.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.62...1.0.63)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 09:30:16 -07:00
LGUG2Z
0f9c23b6f4 feat(cli): add toggle-transparency cmd 2024-07-17 17:25:22 -07:00
LGUG2Z
6ea71834a1 fix(animation): disable on cross-monitor drag
This commit adds an edge case missed in
50a279239a.
2024-07-15 17:30:30 -07:00
LGUG2Z
81451cb17a refactor(client): use public interface exclusively
This commit demotes the komorebi-core crate to a module (core) inside of
the komorebi lib, resulting in the komorebi-client crate lib becoming
the single public interface for programming in Rust against komorebi.

komorebic and komorebi-gui now consume komorebi-client exclusively as
the means for sending and receiving messages to and from komorebi, so
that anyone wishing to integrate with komorebi will have all of the same
functionality to them as I do.
2024-07-15 17:11:35 -07:00
LGUG2Z
7653495e31 chore(dev): begin v0.1.29-dev 2024-07-15 17:11:35 -07:00
LGUG2Z
0cdce8fc2a chore(release): v0.1.28 2024-07-15 08:59:42 -07:00
LGUG2Z
67f14730d0 ci(github): rm nightly tag before running kokai 2024-07-15 08:58:38 -07:00
LGUG2Z
ef9e734680 fix(wm): handle "rdpudd chained dd" edge case
This commit builds on these changes in win32-display-data:
32a45cebf1p

With the addition of lenient fallbacks when looking up display device
information for "RDPUDD Chained DD" virtual display adapters, komorebi
will now set Monitor.device and Monitor.device_id to "UNKNOWN" as this
virtual mirror display driver will never have a reported DeviceID.

This limitation for "RDPUDD Chained DD" devices is also noted in a
Chromium issue: https://codereview.chromium.org/2557513005/

fix #883
2024-07-13 21:37:59 -07:00
104 changed files with 12236 additions and 2968 deletions

View File

@@ -1,52 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]: Short descriptive title"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See bug
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots and Videos**
Add screenshots and videos to help explain your problem.
**Operating System**
Provide the output of `systeminfo | grep "^OS Name\|^OS Version"`
For example:
```
OS Name: Microsoft Windows 11 Pro
OS Version: 10.0.22000 N/A Build 22000
```
**`komorebic check` Output**
Provide the output of `komorebic check`
For example:
```
No KOMOREBI_CONFIG_HOME detected, defaulting to C:\Users\LGUG2Z
Looking for configuration files in C:\Users\LGUG2Z
No komorebi configuration found in C:\Users\LGUG2Z
If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration
```
**Additional context**
Add any other context about the problem here.
In particular, if you have any other AHK scripts or software running that handle any aspect of window management or manipulation

56
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Bug report
description: File a bug report
labels: [bug]
title: "[BUG]: "
body:
- type: textarea
validations:
required: true
attributes:
label: Summary
description: >
Please provide a short summary of the bug, along with any information
you feel is relevant to replicating the bug.
You may include screenshots and videos in this section.
- type: textarea
validations:
required: true
attributes:
label: Version Information
description: >
Please provide information about the versions of Windows and komorebi
running on your machine.
Do not submit a bug if you are not using an official version of Windows
such as AtlasOS; only official versions of Windows are supported.
```
systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version"
```
```
komorebic --version
```
- type: textarea
validations:
required: true
attributes:
label: Komorebi Configuration
description: >
Please provide your configuration file (komorebi.json or komorebi.bar.json)
render: json
- type: textarea
validations:
required: true
attributes:
label: Hotkey Configuration
description: >
Please provide your whkdrc or komorebi.ahk hotkey configuration file
- type: textarea
validations:
required: true
attributes:
label: Output of komorebic check
description: >
Please provide the output of `komorebic check`

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Komorebi Documentation
url: https://lgug2z.github.io/komorebi/
about: Please search the documentation website before opening an issue
- name: Komorebi Quickstart Tutorial Video
url: https://www.youtube.com/watch?v=MMZUAtHbTYY
about: If you are new, please make sure you watch the quickstart tutorial video

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEAT]: Short descriptive title"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,37 @@
name: Feature request
description: Suggest a new feature
labels: [enhancement]
title: "[FEAT]: "
body:
- type: dropdown
id: Sponsors
attributes:
label: Sponsorship Information
description: >
Feature requests are considered from individuals who are $5+ monthly sponsors to the project.
Please specify the platform you use to sponsor the project.
options:
- GitHub Sponsors
- Ko-fi
- Discord
- YouTube
default: 0
validations:
required: true
- type: textarea
validations:
required: true
attributes:
label: Suggestion
description: >
Please share your suggestion here. Be sure to include all necessary context.
If you sponsor on a platform where you use a different username, please specify the username here.
- type: textarea
validations:
required: true
attributes:
label: Alternatives Considered
description: >
Please share share alternatives you have considered here.

View File

@@ -18,105 +18,79 @@ on:
jobs:
build:
name: Build
runs-on: windows-latest
strategy:
fail-fast: true
matrix:
platform:
- os-name: Windows-x86_64
runs-on: windows-latest
target: x86_64-pc-windows-msvc
- os-name: Windows-aarch64
runs-on: windows-latest
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.platform.runs-on }}
permissions: write-all
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
matrix:
target:
- x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Prep cargo dirs
run: |
New-Item "${env:USERPROFILE}\.cargo\registry" -ItemType Directory -Force
New-Item "${env:USERPROFILE}\.cargo\git" -ItemType Directory -Force
shell: powershell
- name: Set environment variables appropriately for the build
run: |
echo "%USERPROFILE%\.cargo\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
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@v4
- run: rustup toolchain install stable --profile minimal
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Get rustc commit hash
id: cargo-target-cache
run: |
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
shell: bash
- name: Cache cargo build
uses: actions/cache@v4
cache-on-failure: "true"
cache-all-crates: "true"
- run: cargo +nightly fmt --check
- run: cargo clippy
- uses: houseabsolute/actions-rust-cross@v0
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') }}
restore-keys: ${{ github.base_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install Rustup using win.rustup.rs
run: |
# Disable the download progress bar which can cause perf issues
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
shell: powershell
- name: Ensure stable toolchain is up to date
run: rustup update stable
shell: bash
- name: Install the target
run: |
rustup target install ${{ matrix.target }}
- name: Run Cargo checks
run: |
cargo fmt --check
cargo check
cargo clippy
- name: Run a full build
run: |
cargo build --locked --release --target ${{ matrix.target }}
- name: Create MSI installer
run: |
command: "build"
target: ${{ matrix.platform.target }}
args: "--locked --release"
- run: |
cargo install cargo-wix
cargo wix -p komorebi --nocapture -I .\wix\main.wxs --target x86_64-pc-windows-msvc
- name: Upload the built artifacts
uses: actions/upload-artifact@v4
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: komorebi-${{ matrix.target }}
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: |
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebic-no-console.exe
target/${{ matrix.target }}/release/komorebi-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
target/${{ matrix.target }}/release/komorebi_gui.pdb
target/${{ matrix.platform.target }}/release/*.exe
target/${{ matrix.platform.target }}/release/*.pdb
target/wix/komorebi-*.msi
retention-days: 7
- name: Check GoReleaser
uses: goreleaser/goreleaser-action@v3
env:
GORELEASER_CURRENT_TAG: v0.1.28
retention-days: 14
nightly:
needs: build
runs-on: windows-latest
permissions: write-all
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
version: latest
args: build --skip=validate --clean
- name: Prepare nightly artifacts
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- shell: bash
run: |
Compress-Archive .\target\${{ matrix.target }}\release\*.exe komorebi-nightly-x86_64-pc-windows-msvc.zip
Copy-Item ./target/wix/*.msi -Destination ./komorebi-nightly-x86_64.msi
echo "$((Get-FileHash komorebi-nightly-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-nightly-x86_64-pc-windows-msvc.zip" >checksums.txt
- name: Update nightly
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
shell: bash
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
git tag nightly
kokai release --no-emoji --add-links github:commits,issues --ref nightly >"CHANGELOG.md"
- shell: bash
run: |
gh release delete nightly --yes || true
git push origin :nightly || true
@@ -124,38 +98,100 @@ jobs:
--target $GITHUB_SHA \
--prerelease \
--title "komorebi nightly (${GITHUB_SHA})" \
--notes "This nightly release of komorebi corresponds to [this commit](https://github.com/LGUG2Z/komorebi/commit/${GITHUB_SHA})." \
--notes-file CHANGELOG.md
komorebi-nightly-x86_64-pc-windows-msvc.zip \
komorebi-nightly-x86_64.msi \
komorebi-nightly-aarch64-pc-windows-msvc.zip \
komorebi-nightly-aarch64.msi \
checksums.txt
# Release
- name: Generate changelog
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
release-dry-run:
needs: build
runs-on: windows-latest
permissions: write-all
if: ${{ github.ref == 'refs/heads/master' }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
kokai release --no-emoji --add-links github:commits,issues --ref "${{ github.ref_name }}" >"CHANGELOG.md"
- uses: softprops/action-gh-release@v2
with:
draft: true
body_path: "CHANGELOG.md"
files: |
checksums.txt
*.zip
*.msi
release:
needs: build
runs-on: windows-latest
permissions: write-all
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
if: startsWith(github.ref, 'refs/tags/v')
- uses: softprops/action-gh-release@v2
with:
version: latest
args: release --skip=validate --clean --release-notes=CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
- name: Add MSI to release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: "target/wix/komorebi-*.msi"
body_path: "CHANGELOG.md"
files: |
checksums.txt
*.zip
*.msi
winget:
name: Publish to WinGet
runs-on: ubuntu-latest
needs: build
needs: release
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
CHANGELOG.md
dummy.go
komorebic/applications.yaml
/.vs

View File

@@ -1,59 +0,0 @@
# Adapted from https://jondot.medium.com/shipping-rust-binaries-with-goreleaser-d5aa42a46be0
project_name: komorebi
before:
hooks:
- powershell.exe -Command "New-Item -Path . -Name dummy.go -ItemType file -Force"
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'package main'"
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'func main() {}'"
builds:
- id: komorebi
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
- id: komorebic
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
- id: komorebic-no-console
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic-no-console
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
- id: komorebi-gui
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-gui
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
format: zip
files:
- LICENSE.md
- CHANGELOG.md
checksum:
name_template: checksums.txt
changelog:
sort: asc

2871
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,27 +4,44 @@ resolver = "2"
members = [
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes"
]
[workspace.dependencies]
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.29"
egui_extras = "0.29"
dirs = "5"
dunce = "1"
hotwatch = "0.5"
schemars = "0.8"
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
sysinfo = "0.30"
serde_yaml = "0.9"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.31"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "32a45cebf132c3d651ee22c0c40033a6b7edc945" }
windows-implement = { version = "0.53" }
windows-interface = { version = "0.53" }
shadow-rs = "0.29"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.35"
which = "6"
[workspace.dependencies.windows]
version = "0.54"
version = "0.58"
features = [
"implement",
"Win32_System_Com",
@@ -42,5 +59,7 @@ features = [
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming"
"Win32_System_WindowsProgramming",
"Media",
"Media_Control"
]

View File

@@ -1,6 +1,6 @@
# PolyForm Strict License 1.0.0
# Komorebi License
<https://polyformproject.org/licenses/strict/1.0.0>
Version 1.0.0
## Acceptance
@@ -13,8 +13,14 @@ your licenses.
The licensor grants you a copyright license for the software
to do everything you might do with the software that would
otherwise infringe the licensor's copyright in it for any
permitted purpose, other than distributing the software or
making changes or new works based on the software.
permitted purpose. However, you may only make changes according
to the [Changes License](#changes-license), and you may not
distribute the software or new works based on the software.
## Changes License
The licensor grants you an additional copyright license to
make changes for any permitted purpose.
## Patent License
@@ -22,10 +28,6 @@ The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for
@@ -34,15 +36,6 @@ entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
## Fair Use
You may have "fair use" rights for the software under the

8
PRIVACY.md Normal file
View File

@@ -0,0 +1,8 @@
# Privacy Policy for Komorebi
No data about your device(s) or _komorebi_ usage leave your device.
## Data Maintained by Komorebi
_komorebi_ writes log files to and keeps a list of temporary window handles (HWNDs) currently managed by the process in
the `$Env:LOCALAPPDATA\komorebi\` directory. This directory is owned by the user running the process.

View File

@@ -82,7 +82,7 @@ A [detailed installation and quickstart
guide](https://lgug2z.github.io/komorebi/installation.html) is available which shows how to get started
using `scoop`, `winget` or building from source.
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/H9-_c1egQ4g/hqdefault.jpg)](https://www.youtube.com/watch?v=H9-_c1egQ4g)
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/MMZUAtHbTYY/hqdefault.jpg)](https://www.youtube.com/watch?v=MMZUAtHbTYY)
# Comparison With Fancy Zones
@@ -99,7 +99,7 @@ video will answer the majority of your questions.
# Demonstrations
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28-dev.0` running on Windows 11 with window borders,
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
unfocused window transparency and animations enabled, using a custom status bar integrated using
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
@@ -191,7 +191,8 @@ required.
## License
`komorebi` is licensed under the [PolyForm Strict 1.0.0
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
is a fork of the [PolyForm Strict 1.0.0
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
this means that you are free to do whatever you want with `komorebi` other than
redistribution, or distribution of new works (ie. hard-forks) based on the
@@ -279,7 +280,7 @@ If the named pipe exists, `komorebi` will start pushing JSON data of successfull
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
in `komorebi-core`.
in `komorebi::core`.
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
@@ -358,7 +359,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.29"}
use anyhow::Result;
use komorebi_client::Notification;
@@ -414,7 +415,7 @@ A TCP listener can optionally be exposed on a port of your choosing with the `--
provided to `komorebi` or `komorebic start`, no TCP listener will be created.
Once created, your client may send
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi-core/src/lib.rs#L37) to `komorebi` in the
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi/src/core/mod.rs#L37) to `komorebi` in the
same way that `komorebic` would.
This can be used if you would like to create your own alternative to `komorebic` which incorporates scripting and

View File

@@ -0,0 +1,16 @@
# animation-duration
```
Set the duration for movement animations in ms
Usage: komorebic.exe animation-duration <DURATION>
Arguments:
<DURATION>
Desired animation durations in ms
Options:
-h, --help
Print help
```

16
docs/cli/animation-fps.md Normal file
View File

@@ -0,0 +1,16 @@
# animation-fps
```
Set the frames per second for movement animations
Usage: komorebic.exe animation-fps <FPS>
Arguments:
<FPS>
Desired animation frames per second
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,20 @@
# animation-style
```
Set the ease function for movement animations
Usage: komorebic.exe animation-style [OPTIONS]
Options:
-s, --style <STYLE>
Desired ease function for animation
[default: linear]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
-h, --help
Print help
```

16
docs/cli/animation.md Normal file
View File

@@ -0,0 +1,16 @@
# animation
```
Enable or disable movement animations
Usage: komorebic.exe animation <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-h, --help
Print help
```

View File

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

View File

@@ -1,7 +1,7 @@
# complete-configuration
```
Signal that the final configuration option has been sent
For legacy komorebi.ahk or komorebi.ps1 configurations, signal that the final configuration option has been sent
Usage: komorebic.exe complete-configuration

View File

@@ -18,6 +18,9 @@ Options:
--ahk
Enable autostart of ahk
--bar
Enable autostart of komorebi-bar
-h, --help
Print help

View File

@@ -0,0 +1,16 @@
# focus-stack-window
```
Focus the specified window index in the focused stack
Usage: komorebic.exe focus-stack-window <TARGET>
Arguments:
<TARGET>
Target index (zero-indexed)
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# reload-configuration
```
Reload ~/komorebi.ahk (if it exists)
Reload legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
Usage: komorebic.exe reload-configuration

View File

@@ -0,0 +1,16 @@
# replace-configuration
```
Replace the configuration of a running instance of komorebi from a static configuration file
Usage: komorebic.exe replace-configuration <PATH>
Arguments:
<PATH>
Static configuration JSON file from which the configuration should be loaded
Options:
-h, --help
Print help
```

View File

@@ -24,6 +24,9 @@ Options:
--ahk
Start autohotkey configuration file
--bar
Start komorebi-bar in a background process
-h, --help
Print help

View File

@@ -9,6 +9,9 @@ Options:
--whkd
Stop whkd if it is running as a background process
--bar
Stop komorebi-bar if it is running as a background process
-h, --help
Print help

View File

@@ -0,0 +1,12 @@
# toggle-transparency
```
Toggle transparency for unfocused windows
Usage: komorebic.exe toggle-transparency
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# watch-configuration
```
Enable or disable watching of ~/komorebi.ahk (if it exists)
Enable or disable watching of legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
Usage: komorebic.exe watch-configuration <BOOLEAN_STATE>

View File

@@ -16,16 +16,17 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart
```
With the example configurations downloaded, you can now start `komorebi` and `whkd.
With the example configurations downloaded, you can now start `komorebi`,
`komorebi-bar` and `whkd`.
```powershell
komorebic start --whkd
komorebic start --whkd --bar
```
## komorebi.json
The example window manager configuration sets some sane defaults and provides
five preconfigured workspaces on the primary monitor each with a different
seven preconfigured workspaces on the primary monitor each with a different
layout.
```json
@@ -213,3 +214,24 @@ reference.
If you want to use one of those key codes, put them into lower case and remove
the `VK_` prefix. For example, the keycode `VK_OEM_PLUS` becomes `oem_plus` in
the sample configuration above.
## komorebi.bar.json
The example status bar configuration sets some sane defaults and provides
a number of pre-configured widgets on the primary monitor.
```json
{% include "./komorebi.bar.example.json" %}
```
### Themes
Themes can be set in either `komorebi.json` or `komorebi.bar.json`. If set
in `komorebi.json`, the theme will be applied to both komorebi's borders and
stackbars as well as the status bar.
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
All [Catppuccin palette variants](https://catppuccin.com/)
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
are available as themes.

View File

@@ -1,6 +1,6 @@
# Getting started
`komorebi` is a tiling window manager for Windows that is comprised of two
`komorebi` is a tiling window manager for Windows that is comprised of two
main binaries, `komorebi.exe`, which contains the window manager itself,
and `komorebic.exe`, which is the main way to send commands to the tiling
window manager.
@@ -23,6 +23,10 @@ suggest that once you are familiar with the main `komorebic.exe` commands used
to manipulate the window manager, you use
[AutoHotKey](https://www.autohotkey.com/) to handle your key bindings.
`komorebi` also includes `komorebi-bar.exe`, a simple and reliable status bar which
is deeply integrated with the tiling window manager, and can be customized with
various widgets and themes.
## Installation
`komorebi` is available pre-built to install via
@@ -115,6 +119,7 @@ cargo +stable install --path komorebi --locked
cargo +stable install --path komorebic --locked
cargo +stable install --path komorebic-no-console --locked
cargo +stable install --path komorebi-gui --locked
cargo +stable install --path komorebi-bar --locked
```
If the binaries have been built and added to your `$PATH` correctly, you should
@@ -131,8 +136,8 @@ first-time set up and running komorebi require an internet connection).
## Uninstallation
Before uninstalling, first run `komorebic stop --whkd` to make sure that both
the `komorebi` and `whkd` processes have been stopped.
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
the `komorebi`, `komorebi-bar` and `whkd` processes have been stopped.
Then, depending on whether you installed with Scoop or WinGet, run `scoop
uninstall komorebi whkd` or `winget uninstall LGUG2Z.komorebi LGUG2Z.whkd`.

View File

@@ -0,0 +1,76 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {
"left": 0,
"top": 40,
"right": 0,
"bottom": 40
}
},
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",
"name": "Ashes",
"accent": "Base0D"
},
"left_widgets": [
{
"Komorebi": {
"workspaces": {
"enable": true,
"hide_empty_workspaces": false
},
"layout": {
"enable": true
},
"focused_window": {
"enable": true,
"show_icon": true
}
}
}
],
"right_widgets": [
{
"Media": {
"enable": true
}
},
{
"Storage": {
"enable": true
}
},
{
"Memory": {
"enable": true
}
},
{
"Network": {
"enable": true,
"show_total_data_transmitted": true,
"show_network_activity": true
}
},
{
"Date": {
"enable": true,
"format": "DayDateMonthYear"
}
},
{
"Time": {
"enable": true,
"format": "TwentyFourHour"
}
},
{
"Battery": {
"enable": true
}
}
]
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.28/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
@@ -8,20 +8,17 @@
"border": true,
"border_width": 8,
"border_offset": -1,
"border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399",
"unfocused": "#808080"
"theme": {
"palette": "Base16",
"name": "Ashes",
"unfocused_border": "Base03",
"bar_accent": "Base0D"
},
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300,
"focused_text": "#00a542",
"unfocused_text": "#b3b3b3",
"background": "#141414"
"width": 300
}
},
"monitors": [

View File

@@ -7,22 +7,24 @@ clean:
fmt:
cargo +nightly fmt
cargo +stable clippy
prettier --write README.md
prettier --write .goreleaser.yml
prettier --write .github/ISSUE_TEMPLATE/bug_report.yml
prettier --write .github/ISSUE_TEMPLATE/config.yml
prettier --write .github/ISSUE_TEMPLATE/feature_request.yml
prettier --write .github/dependabot.yml
prettier --write .github/FUNDING.yml
prettier --write .github/workflows/windows.yaml
install-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
install-target target:
cargo +stable install --path {{ target }} --locked
install:
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
run:
cargo +stable run --bin komorebi --locked
run target:
cargo +stable run --bin {{ target }} --locked
warn $RUST_LOG="warn":
just run
@@ -40,9 +42,16 @@ deadlock $RUST_LOG="trace":
cargo +stable run --bin komorebi --locked --features deadlock_detection
docgen:
komorebic docgen
cargo run --package komorebic -- docgen
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
schemagen:
komorebic static-config-schema > schema.json
cargo run --package komorebic -- static-config-schema > schema.json
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
cargo run --package komorebi-bar -- --schema > schema.bar.json
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
rm -Force .\bar-config-docs\schema.html
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html

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

@@ -0,0 +1,37 @@
[package]
name = "komorebi-bar"
version = "0.1.30"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-client = { path = "../komorebi-client" }
komorebi-themes = { path = "../komorebi-themes" }
chrono = { workspace = true }
clap = { workspace = true }
color-eyre = { workspace = true }
crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = "0.7"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.31"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }

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

@@ -0,0 +1,458 @@
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::process_hwnd;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::BAR_HEIGHT;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crossbeam_channel::Receiver;
use eframe::egui::Align;
use eframe::egui::CentralPanel;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontData;
use eframe::egui::FontDefinitions;
use eframe::egui::FontFamily;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Layout;
use eframe::egui::Margin;
use eframe::egui::Style;
use eframe::egui::TextStyle;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct Komobar {
pub config: Arc<KomobarConfig>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_config: Receiver<KomobarConfig>,
pub bg_color: Rc<RefCell<Color32>>,
pub scale_factor: f32,
}
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
match theme {
KomobarTheme::Catppuccin {
name: catppuccin,
accent: catppuccin_value,
} => match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::FRAPPE.base);
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
},
KomobarTheme::Base16 {
name: base16,
accent: base16_value,
} => {
ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(base16);
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
});
bg_color.replace(base16.background());
}
}
}
impl Komobar {
pub fn apply_config(
&mut self,
ctx: &Context,
config: &KomobarConfig,
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
) {
MAX_LABEL_WIDTH.store(
config.max_label_width.unwrap_or(400.0) as i32,
Ordering::SeqCst,
);
if let Some(font_family) = &config.font_family {
tracing::info!("attempting to add custom font family: {font_family}");
Self::add_custom_font(ctx, font_family);
}
let position = config.position.clone().unwrap_or(PositionConfig {
start: Some(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
}),
end: Some(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
}),
});
if let Some(hwnd) = process_hwnd() {
let start = position.start.unwrap_or(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
});
let end = position.end.unwrap_or(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
});
let rect = komorebi_client::Rect {
left: start.x as i32,
top: start.y as i32,
right: end.x as i32,
bottom: end.y as i32,
};
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
match config.theme {
Some(theme) => {
apply_theme(ctx, theme, self.bg_color.clone());
}
None => {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
let config = home_dir.join("komorebi.json");
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), self.bg_color.clone());
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
name, stack_border, ..
} => stack_border
.unwrap_or(CatppuccinValue::Green)
.color32(name.as_theme()),
KomorebiTheme::Base16 {
name, stack_border, ..
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
};
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
}
}
Err(_) => {
ctx.set_style(Style::default());
self.bg_color.replace(Style::default().visuals.panel_fill);
}
}
}
}
if let Some(font_size) = &config.font_size {
tracing::info!("attempting to set custom font size: {font_size}");
Self::set_font_size(ctx, *font_size);
}
let mut komorebi_widget = None;
let mut komorebi_widget_idx = None;
let mut komorebi_notification_state = previous_notification_state;
let mut side = None;
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Left);
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Right);
}
}
let mut left_widgets = config
.left_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut right_widgets = config
.right_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if let (Some(idx), Some(mut widget), Some(side)) =
(komorebi_widget_idx, komorebi_widget, side)
{
match komorebi_notification_state {
None => {
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
previous
.borrow_mut()
.update_from_config(&widget.komorebi_notification_state.borrow());
widget.komorebi_notification_state = previous.clone();
}
}
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Side::Left => left_widgets[idx] = boxed,
Side::Right => right_widgets[idx] = boxed,
}
}
right_widgets.reverse();
self.left_widgets = left_widgets;
self.right_widgets = right_widgets;
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
}
pub fn new(
cc: &eframe::CreationContext<'_>,
rx_gui: Receiver<komorebi_client::Notification>,
rx_config: Receiver<KomobarConfig>,
config: Arc<KomobarConfig>,
) -> Self {
let mut komobar = Self {
config: config.clone(),
komorebi_notification_state: None,
left_widgets: vec![],
right_widgets: vec![],
rx_gui,
rx_config,
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
};
komobar.apply_config(&cc.egui_ctx, &config, None);
// needs a double apply the first time for some reason
komobar.apply_config(&cc.egui_ctx, &config, None);
komobar
}
fn set_font_size(ctx: &Context, font_size: f32) {
ctx.style_mut(|style| {
style.text_styles = [
(TextStyle::Small, FontId::new(9.0, FontFamily::Proportional)),
(
TextStyle::Body,
FontId::new(font_size, FontFamily::Proportional),
),
(
TextStyle::Button,
FontId::new(font_size, FontFamily::Proportional),
),
(
TextStyle::Heading,
FontId::new(18.0, FontFamily::Proportional),
),
(
TextStyle::Monospace,
FontId::new(font_size, FontFamily::Monospace),
),
]
.into();
});
}
fn add_custom_font(ctx: &Context, name: &str) {
let mut fonts = FontDefinitions::default();
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
let property = FontPropertyBuilder::new().family(name).build();
if let Some((font, _)) = system_fonts::get(&property) {
fonts
.font_data
.insert(name.to_owned(), FontData::from_owned(font));
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.insert(0, name.to_owned());
fonts
.families
.entry(FontFamily::Monospace)
.or_default()
.push(name.to_owned());
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
}
}
impl eframe::App for Komobar {
// TODO: I think this is needed for transparency??
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
// egui::Rgba::TRANSPARENT.to_array()
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
// background[3] = 0.9;
// background
// }
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(
ctx,
&self.config.clone(),
self.komorebi_notification_state.clone(),
);
}
if let Ok(updated_config) = self.rx_config.try_recv() {
self.apply_config(
ctx,
&updated_config,
self.komorebi_notification_state.clone(),
);
}
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(
ctx,
self.config.monitor.index,
self.rx_gui.clone(),
self.bg_color.clone(),
);
}
let frame = if let Some(frame) = &self.config.frame {
Frame::none()
.inner_margin(Margin::symmetric(
frame.inner_margin.x,
frame.inner_margin.y,
))
.fill(*self.bg_color.borrow())
} else {
Frame::none().fill(*self.bg_color.borrow())
};
CentralPanel::default().frame(frame).show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui);
}
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui);
}
})
})
});
}
}
#[derive(Copy, Clone)]
enum Side {
Left,
Right,
}

160
komorebi-bar/src/battery.rs Normal file
View File

@@ -0,0 +1,160 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager;
use starship_battery::State;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<BatteryConfig> for Battery {
fn from(value: BatteryConfig) -> Self {
let manager = Manager::new().unwrap();
let mut last_state = String::new();
let mut state = None;
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(mut batteries) = manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => state = Some(BatteryState::Charging),
State::Discharging => state = Some(BatteryState::Discharging),
_ => {}
}
last_state = match prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
Self {
enable: value.enable,
manager,
last_state,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
state: state.unwrap_or(BatteryState::Discharging),
last_updated: Instant::now(),
}
}
}
pub enum BatteryState {
Charging,
Discharging,
}
pub struct Battery {
pub enable: bool,
manager: Manager,
pub state: BatteryState,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_state: String,
last_updated: Instant,
}
impl Battery {
fn output(&mut self) -> String {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output.clear();
if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => self.state = BatteryState::Discharging,
_ => {}
}
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
}
impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let emoji = match self.state {
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
};
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
}
ui.add_space(WIDGET_SPACING);
}
}
}

178
komorebi-bar/src/config.rs Normal file
View File

@@ -0,0 +1,178 @@
use crate::widget::WidgetConfig;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::KomorebiTheme;
use komorebi_client::Rect;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
pub struct KomobarConfig {
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// Monitor options
pub monitor: MonitorConfig,
/// Font family
pub font_family: Option<String>,
/// Font size (default: 12.5)
pub font_size: Option<f32>,
/// Max label width before text truncation (default: 400.0)
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
impl KomobarConfig {
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("position", ["viewport"]);
map.insert("end", ["inner_frame"]);
let mut display = false;
for aliases in map.values() {
for a in aliases {
if raw.contains(a) {
display = true;
}
}
}
if display {
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
println!(r#""{alias}" is now "{canonical}""#);
}
}
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
#[serde(alias = "position")]
pub start: Option<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
#[serde(alias = "inner_size")]
pub end: Option<Position>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct FrameConfig {
/// Margin inside the painted frame
pub inner_margin: Position,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub index: usize,
/// Automatically apply a work area offset for this monitor to accommodate the bar
pub work_area_offset: Option<Rect>,
}
impl KomobarConfig {
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = match path.extension().unwrap().to_string_lossy().as_str() {
"json" => serde_json::from_str(&content)?,
_ => panic!("unsupported format"),
};
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position { x: 10.0, y: 10.0 },
});
}
Ok(value)
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Position {
/// X coordinate
pub x: f32,
/// Y coordinate
pub y: f32,
}
impl From<Position> for Vec2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
impl From<Position> for Pos2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "palette")]
pub enum KomobarTheme {
/// A theme from catppuccin-egui
Catppuccin {
name: komorebi_themes::Catppuccin,
accent: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
},
}
impl From<KomorebiTheme> for KomobarTheme {
fn from(value: KomorebiTheme) -> Self {
match value {
KomorebiTheme::Catppuccin {
name, bar_accent, ..
} => Self::Catppuccin {
name,
accent: bar_accent,
},
KomorebiTheme::Base16 {
name, bar_accent, ..
} => Self::Base16 {
name,
accent: bar_accent,
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum LabelPrefix {
/// Show no prefix
None,
/// Show an icon
Icon,
/// Show text
Text,
/// Show an icon and text
IconAndText,
}

120
komorebi-bar/src/cpu.rs Normal file
View File

@@ -0,0 +1,120 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CpuConfig {
/// Enable the Cpu widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<CpuConfig> for Cpu {
fn from(value: CpuConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
system.refresh_cpu_usage();
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Cpu {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Cpu {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage();
self.last_updated = now;
}
let used = self.system.global_cpu_usage();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
}
}
}
impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CPU.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

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

@@ -0,0 +1,137 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DateConfig {
/// Enable the Date widget
pub enable: bool,
/// Set the Date format
pub format: DateFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<DateConfig> for Date {
fn from(value: DateConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum DateFormat {
/// Month/Date/Year format (09/08/24)
MonthDateYear,
/// Year-Month-Date format (2024-09-08)
YearMonthDate,
/// Date-Month-Year format (8-Sep-2024)
DateMonthYear,
/// Day Date Month Year format (8 September 2024)
DayDateMonthYear,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
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"),
DateFormat::Custom(custom) => custom.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct Date {
pub enable: bool,
pub format: DateFormat,
label_prefix: LabelPrefix,
}
impl Date {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
}
}
impl BarWidget for Date {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CALENDAR_DOTS.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "DATE: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.next()
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -0,0 +1,546 @@
use crate::bar::apply_theme;
use crate::config::KomobarTheme;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::text::LayoutJob;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::SelectableLabel;
use eframe::egui::Sense;
use eframe::egui::TextStyle;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use image::RgbaImage;
use komorebi_client::CycleDirection;
use komorebi_client::NotificationEvent;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: KomorebiWorkspacesConfig,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Focused Window widget
pub focused_window: Option<KomorebiFocusedWindowConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
pub enable: bool,
/// Show the icon of the currently focused window
pub show_icon: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
/// A map of display friendly name => path to configuration.json
pub configurations: BTreeMap<String, String>,
}
impl From<&KomorebiConfig> for Komorebi {
fn from(value: &KomorebiConfig) -> Self {
let configuration_switcher =
if let Some(configuration_switcher) = &value.configuration_switcher {
let mut configuration_switcher = configuration_switcher.clone();
for (_, location) in configuration_switcher.configurations.iter_mut() {
*location = dunce::simplified(&PathBuf::from(location.clone()))
.to_string_lossy()
.to_string();
}
Some(configuration_switcher)
} else {
None
};
Self {
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
selected_workspace: String::new(),
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
workspaces: vec![],
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: (vec![], vec![], 0),
stack_accent: None,
})),
workspaces: value.workspaces,
layout: value.layout,
focused_window: value.focused_window,
configuration_switcher,
}
}
}
#[derive(Clone, Debug)]
pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: KomorebiWorkspacesConfig,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
impl BarWidget for Komorebi {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
if self.workspaces.enable {
let mut update = None;
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
if ui
.add(SelectableLabel::new(
komorebi_notification_state.selected_workspace.eq(ws),
ws.to_string(),
))
.clicked()
{
update = Some(ws.to_string());
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
.is_err()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
.is_err()
{
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::RetileWithResizeDimensions)
.is_err()
{
tracing::error!("could not send message to komorebi: Retile");
}
}
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
ui.add_space(WIDGET_SPACING);
}
if let Some(layout) = self.layout {
if layout.enable {
if ui
.add(
Label::new(komorebi_notification_state.layout.to_string())
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
match komorebi_notification_state.layout {
KomorebiLayout::Default(_) => {
if komorebi_client::send_message(&SocketMessage::CycleLayout(
CycleDirection::Next,
))
.is_err()
{
tracing::error!("could not send message to komorebi: CycleLayout");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err()
{
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
ui.add_space(WIDGET_SPACING);
}
}
if let Some(configuration_switcher) = &self.configuration_switcher {
if configuration_switcher.enable {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file()
&& ui
.add(Label::new(name).selectable(false).sense(Sense::click()))
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
canonicalized,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
proceed = false;
}
if let Some(rect) = komorebi_notification_state.work_area_offset {
if proceed {
match komorebi_client::send_query(&SocketMessage::Query(
komorebi_client::StateQuery::FocusedMonitorIndex,
)) {
Ok(idx) => {
if let Ok(monitor_idx) = idx.parse::<usize>() {
if komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
rect,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MonitorWorkAreaOffset"
);
}
}
}
Err(_) => {
tracing::error!(
"could not send message to komorebi: Query"
);
}
}
}
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
let titles = &komorebi_notification_state.focused_container_information.0;
let icons = &komorebi_notification_state.focused_container_information.1;
let focused_window_idx =
komorebi_notification_state.focused_container_information.2;
let iter = titles.iter().zip(icons.iter());
for (i, (title, icon)) in iter.enumerate() {
if focused_window.show_icon {
if let Some(img) = icon {
ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.max_height(15.0),
);
}
}
if i == focused_window_idx {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let layout_job = LayoutJob::simple(
title.to_string(),
font_id.clone(),
komorebi_notification_state
.stack_accent
.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
if titles.len() > 1 {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
);
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title).selectable(false).truncate(),
);
}
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
);
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
ctx.load_texture("icon", color_image, TextureOptions::default())
}
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<String>,
pub selected_workspace: String,
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
pub layout: KomorebiLayout,
pub hide_empty_workspaces: bool,
pub mouse_follows_focus: bool,
pub work_area_offset: Option<Rect>,
pub stack_accent: Option<Color32>,
}
#[derive(Copy, Clone, Debug)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Floating,
Paused,
Custom,
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiNotificationState {
pub fn update_from_config(&mut self, config: &Self) {
self.hide_empty_workspaces = config.hide_empty_workspaces;
}
pub fn handle_notification(
&mut self,
ctx: &Context,
monitor_index: usize,
rx_gui: Receiver<komorebi_client::Notification>,
bg_color: Rc<RefCell<Color32>>,
) {
match rx_gui.try_recv() {
Err(error) => match error {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
tracing::error!(
"failed to receive komorebi notification on gui thread: {error}"
);
}
},
Ok(notification) => {
if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) =
notification.event
{
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
tracing::info!("applied theme from updated komorebi.json");
}
}
}
self.mouse_follows_focus = notification.state.mouse_follows_focus;
let monitor = &notification.state.monitors.elements()[monitor_index];
self.work_area_offset =
notification.state.monitors.elements()[monitor_index].work_area_offset();
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_add = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
} else {
true
};
if should_add {
workspaces
.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
}
}
self.workspaces = workspaces;
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
}
if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
}
if let Some(container) =
monitor.workspaces()[focused_workspace_idx].monocle_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else if let Some(container) =
monitor.workspaces()[focused_workspace_idx].focused_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else {
self.focused_container_information.0.clear();
self.focused_container_information.1.clear();
self.focused_container_information.2 = 0;
}
}
}
}
}

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

@@ -0,0 +1,411 @@
mod bar;
mod battery;
mod config;
mod cpu;
mod date;
mod komorebi;
mod media;
mod memory;
mod network;
mod storage;
mod time;
mod ui;
mod widget;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
use crate::config::Position;
use crate::config::PositionConfig;
use clap::Parser;
use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
pub static WIDGET_SPACING: f32 = 10.0;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
/// Print the JSON schema of the configuration file and exit
#[clap(long)]
schema: bool,
/// Print a list of fonts available on this system and exit
#[clap(long)]
fonts: bool,
/// Path to a JSON or YAML configuration file
#[clap(short, long)]
config: Option<PathBuf>,
/// Write an example komorebi.bar.json to disk
#[clap(long)]
quickstart: bool,
/// Print a list of aliases that can be renamed to canonical variants
#[clap(long)]
#[clap(hide = true)]
aliases: bool,
}
extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
unsafe {
let mut process_id = 0;
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
if process_id == GetCurrentProcessId() {
*(lparam.0 as *mut HWND) = hwnd;
BOOL::from(false) // Stop enumeration
} else {
BOOL::from(true) // Continue enumeration
}
}
}
fn process_hwnd() -> Option<isize> {
unsafe {
let mut hwnd = HWND::default();
let _ = EnumThreadWindows(
GetCurrentThreadId(),
Some(enum_window),
LPARAM(&mut hwnd as *mut HWND as isize),
);
if hwnd.0 as isize == 0 {
None
} else {
Some(hwnd.0 as isize)
}
}
}
fn main() -> color_eyre::Result<()> {
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
let opts: Opts = Opts::parse();
if opts.schema {
let settings = SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
println!("{schema}");
std::process::exit(0);
}
if opts.fonts {
for font in system_fonts::query_all() {
println!("{font}");
}
std::process::exit(0);
}
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
tracing::subscriber::set_global_default(
tracing_subscriber::fmt::Subscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.finish(),
)?;
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
if opts.quickstart {
let komorebi_bar_json = include_str!("../../docs/komorebi.bar.example.json").to_string();
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
println!(
"Example komorebi.bar.json file written to {}",
home_dir.as_path().display()
);
std::process::exit(0);
}
let default_config_path = home_dir.join("komorebi.bar.json");
let config_path = opts.config.map_or_else(
|| {
if !default_config_path.is_file() {
None
} else {
Some(default_config_path.clone())
}
},
Option::from,
);
let mut config = match config_path {
None => {
let komorebi_bar_json =
include_str!("../../docs/komorebi.bar.example.json").to_string();
std::fs::write(&default_config_path, komorebi_bar_json)?;
tracing::info!(
"created example configuration file: {}",
default_config_path.as_path().display()
);
KomobarConfig::read(&default_config_path)?
}
Some(ref config) => {
if !opts.aliases {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
}
KomobarConfig::read(config)?
}
};
let config_path = config_path.unwrap_or(default_config_path);
if opts.aliases {
KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?);
std::process::exit(0);
}
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
&SocketMessage::State,
)?)?;
MONITOR_RIGHT.store(
state.monitors.elements()[config.monitor.index].size().right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().top,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().left,
Ordering::SeqCst,
);
match config.position {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
}),
})
}
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
});
}
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
})
}
}
}
let viewport_builder = ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_taskbar(false);
let native_options = eframe::NativeOptions {
viewport: viewport_builder,
..Default::default()
};
if let Some(rect) = &config.monitor.work_area_offset {
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
config.monitor.index,
*rect,
))?;
tracing::info!(
"work area offset applied to monitor: {}",
config.monitor.index
);
}
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
let (tx_config, rx_config) = crossbeam_channel::unbounded();
let mut hotwatch = Hotwatch::new()?;
let config_path_cl = config_path.clone();
hotwatch.watch(config_path, move |event| match event.kind {
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
Ok(updated) => {
tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
if let Err(error) = tx_config.send(updated) {
tracing::error!("could not send configuration update to gui: {error}")
}
}
Err(error) => {
tracing::error!("{error}");
}
},
_ => {}
})?;
tracing::info!("watching configuration file for changes");
let config_arc = Arc::new(config);
eframe::run_native(
"komorebi-bar",
native_options,
Box::new(|cc| {
let config_cl = config_arc.clone();
let ctx_repainter = cc.egui_ctx.clone();
std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(1));
ctx_repainter.request_repaint();
});
let ctx_komorebi = cc.egui_ctx.clone();
std::thread::spawn(move || {
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
filter_state_changes: true,
})
.expect("could not subscribe to komorebi notifications");
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);
for client in listener.incoming() {
match client {
Ok(subscription) => {
let mut buffer = Vec::new();
let mut reader = BufReader::new(subscription);
// this is when we know a shutdown has been sent
if matches!(reader.read_to_end(&mut buffer), Ok(0)) {
tracing::info!("disconnected from komorebi");
// keep trying to reconnect to komorebi
while komorebi_client::send_message(
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
tracing::info!("reconnected to komorebi");
if let Some(rect) = &config_cl.monitor.work_area_offset {
while komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
config_cl.monitor.index,
*rect,
),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
}
}
match String::from_utf8(buffer) {
Ok(notification_string) => {
match serde_json::from_str::<komorebi_client::Notification>(
&notification_string,
) {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(notification) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}
ctx_komorebi.request_repaint();
}
Err(error) => {
tracing::error!("could not deserialize komorebi notification: {error}");
}
}
}
Err(error) => {
tracing::error!(
"komorebi notification string was invalid utf8: {error}"
)
}
}
}
Err(error) => {
tracing::error!("{error}");
}
}
}
});
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
}

128
komorebi-bar/src/media.rs Normal file
View File

@@ -0,0 +1,128 @@
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
}
impl From<MediaConfig> for Media {
fn from(value: MediaConfig) -> Self {
Self::new(value.enable)
}
}
#[derive(Clone, Debug)]
pub struct Media {
pub enable: bool,
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
}
impl Media {
pub fn new(enable: bool) -> Self {
Self {
enable,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap()
.get()
.unwrap(),
}
}
pub fn toggle(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(op) = session.TryTogglePlayPauseAsync() {
op.get().unwrap_or_default();
}
}
}
fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
if let Ok(properties) = operation.get() {
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
if artist.is_empty() {
return format!("{title}");
}
if title.is_empty() {
return format!("{artist}");
}
return format!("{artist} - {title}");
}
}
}
}
String::new()
}
}
impl BarWidget for Media {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::HEADPHONES.to_string(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
self.toggle();
}
ui.add_space(WIDGET_SPACING);
}
}
}
}

123
komorebi-bar/src/memory.rs Normal file
View File

@@ -0,0 +1,123 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MemoryConfig {
/// Enable the Memory widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<MemoryConfig> for Memory {
fn from(value: MemoryConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
system.refresh_memory();
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Memory {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Memory {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_memory();
self.last_updated = now;
}
let used = self.system.used_memory();
let total = self.system.total_memory();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
}
}
}
impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::MEMORY.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

425
komorebi-bar/src/network.rs Normal file
View File

@@ -0,0 +1,425 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use num_derive::FromPrimitive;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total data transmitted
pub show_total_data_transmitted: bool,
/// Show network activity
pub show_network_activity: bool,
/// Characters to reserve for network activity data
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let mut last_state_data = vec![];
let mut last_state_transmitted = vec![];
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
let mut networks_network_activity = Networks::new_with_refreshed_list();
let mut default_interface = String::new();
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = interface.friendly_name {
default_interface.clone_from(&friendly_name);
if value.show_total_data_transmitted {
networks_total_data_transmitted.refresh();
for (interface_name, data) in &networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
last_state_data.push(match prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
if value.show_network_activity {
networks_network_activity.refresh();
for (interface_name, data) in &networks_network_activity {
if friendly_name.eq(interface_name) {
last_state_transmitted.push(match prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::IconAndText => format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
})
}
}
}
}
}
Self {
enable: value.enable,
networks_total_data_transmitted,
networks_network_activity,
default_interface,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
show_total_data_transmitted: value.show_total_data_transmitted,
show_network_activity: value.show_network_activity,
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_data_transmitted: last_state_data,
last_state_network_activity: last_state_transmitted,
last_updated_total_data_transmitted: Instant::now(),
last_updated_network_activity: Instant::now(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_data_transmitted: bool,
pub show_network_activity: bool,
networks_total_data_transmitted: Networks,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_data_transmitted: Vec<String>,
last_state_network_activity: Vec<String>,
last_updated_total_data_transmitted: Instant,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
impl Network {
fn default_interface(&mut self) {
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
}
}
}
fn network_activity(&mut self) -> Vec<String> {
let mut outputs = self.last_state_network_activity.clone();
let now = Instant::now();
if self.show_network_activity
&& now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_network_activity {
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::IconAndText => {
format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
)
}
})
}
}
}
}
}
self.last_state_network_activity.clone_from(&outputs);
self.last_updated_network_activity = now;
}
outputs
}
fn total_data_transmitted(&mut self) -> Vec<String> {
let mut outputs = self.last_state_total_data_transmitted.clone();
let now = Instant::now();
if self.show_total_data_transmitted
&& now.duration_since(self.last_updated_total_data_transmitted)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_total_data_transmitted {
self.networks_total_data_transmitted.refresh();
for (interface_name, data) in &self.networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
}
}
self.last_state_total_data_transmitted.clone_from(&outputs);
self.last_updated_total_data_transmitted = now;
}
outputs
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.show_total_data_transmitted {
for output in self.total_data_transmitted() {
ui.add(Label::new(output).selectable(false));
}
ui.add_space(WIDGET_SPACING);
}
if self.show_network_activity {
for output in self.network_activity() {
ui.add(Label::new(output).selectable(false));
}
ui.add_space(WIDGET_SPACING);
}
if self.enable {
self.default_interface();
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
#[derive(Debug, FromPrimitive)]
enum DataUnit {
B = 0,
K = 1,
M = 2,
G = 3,
T = 4,
P = 5,
E = 6,
Z = 7,
Y = 8,
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}

134
komorebi-bar/src/storage.rs Normal file
View File

@@ -0,0 +1,134 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<StorageConfig> for Storage {
fn from(value: StorageConfig) -> Self {
Self {
enable: value.enable,
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Storage {
fn output(&mut self) -> Vec<String> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh();
self.last_updated = now;
}
let mut disks = vec![];
for disk in &self.disks {
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
disks.push(match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
})
}
disks.sort();
disks.reverse();
disks
}
}
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
for output in self.output() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::HARD_DRIVES.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{
eprintln!("{}", error)
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
}

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

@@ -0,0 +1,128 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
/// Set the Time format
pub format: TimeFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<TimeConfig> for Time {
fn from(value: TimeConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
/// Twenty-four-hour format (with seconds)
TwentyFourHour,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
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"),
TimeFormat::Custom(format) => format.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
label_prefix: LabelPrefix,
}
impl Time {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
}
}
impl BarWidget for Time {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CLOCK.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "TIME: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.toggle()
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

22
komorebi-bar/src/ui.rs Normal file
View File

@@ -0,0 +1,22 @@
use eframe::egui::Align;
use eframe::egui::Layout;
use eframe::egui::Response;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::Widget;
pub struct CustomUi<'ui>(pub &'ui mut Ui);
impl CustomUi<'_> {
pub fn add_sized_left_to_right(
&mut self,
max_size: impl Into<Vec2>,
widget: impl Widget,
) -> Response {
let layout =
Layout::from_main_dir_and_cross_align(self.0.layout().main_dir(), Align::Center);
self.0
.allocate_ui_with_layout(max_size.into(), layout, |ui| ui.add(widget))
.inner
}
}

View File

@@ -0,0 +1,56 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
use crate::media::MediaConfig;
use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui);
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
Date(DateConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
Memory(MemoryConfig),
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
}
}
}

View File

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

View File

@@ -1,9 +1,36 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement;
pub use komorebi::core::Axis;
pub use komorebi::core::BorderImplementation;
pub use komorebi::core::BorderStyle;
pub use komorebi::core::CustomLayout;
pub use komorebi::core::CycleDirection;
pub use komorebi::core::DefaultLayout;
pub use komorebi::core::Direction;
pub use komorebi::core::FocusFollowsMouseImplementation;
pub use komorebi::core::HidingBehaviour;
pub use komorebi::core::Layout;
pub use komorebi::core::MoveBehaviour;
pub use komorebi::core::OperationBehaviour;
pub use komorebi::core::OperationDirection;
pub use komorebi::core::Rect;
pub use komorebi::core::Sizing;
pub use komorebi::core::SocketMessage;
pub use komorebi::core::StackbarLabel;
pub use komorebi::core::StackbarMode;
pub use komorebi::core::StateQuery;
pub use komorebi::core::WindowKind;
pub use komorebi::monitor::Monitor;
pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
@@ -11,27 +38,15 @@ pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::BorderColours;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::BorderStyle;
pub use komorebi_core::CustomLayout;
pub use komorebi_core::CycleDirection;
pub use komorebi_core::DefaultLayout;
pub use komorebi_core::Direction;
pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
pub use komorebi_core::StackbarLabel;
pub use komorebi_core::StackbarMode;
pub use komorebi_core::WindowKind;
use komorebi::DATA_DIR;
@@ -46,16 +61,10 @@ const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
}
}
Ok(())
let mut stream = UnixStream::connect(socket)?;
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
@@ -89,3 +98,29 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
Ok(listener)
}
pub fn subscribe_with_options(
name: &str,
options: SubscribeOptions,
) -> std::io::Result<UnixListener> {
let socket = DATA_DIR.join(name);
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {}
_ => {
return Err(error);
}
},
};
let listener = UnixListener::bind(&socket)?;
send_message(&SocketMessage::AddSubscriberSocketWithOptions(
name.to_string(),
options,
))?;
Ok(listener)
}

View File

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

View File

@@ -1,14 +1,15 @@
[package]
name = "komorebi-gui"
version = "0.1.28-dev.0"
version = "0.1.30"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
egui_extras = { version = "0.28" }
eframe = "0.28"
komorebi-client = { path = "../komorebi-client" }
serde_json = "1"
eframe = { workspace = true }
egui_extras = { workspace = true }
random_word = { version = "0.4.3", features = ["en"] }
serde_json = { workspace = true }
windows = { workspace = true }

View File

@@ -27,7 +27,6 @@ fn main() {
viewport: ViewportBuilder::default()
.with_always_on_top()
.with_inner_size([320.0, 500.0]),
follow_system_theme: true,
..Default::default()
};
@@ -218,7 +217,7 @@ extern "system" fn enum_window(
lparam: windows::Win32::Foundation::LPARAM,
) -> windows::Win32::Foundation::BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let window = Window::from(hwnd.0);
let window = Window::from(hwnd.0 as isize);
if window.is_window()
&& !window.is_miminized()
@@ -234,7 +233,8 @@ extern "system" fn enum_window(
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
let language = "json";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
let theme =
egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), &ui.ctx().style());
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
}

View File

@@ -0,0 +1,12 @@
[package]
name = "komorebi-themes"
version = "0.1.30"
edition = "2021"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "57c38257cb0c6434321320d3746049bd58c34674" }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f579847bf2f552b144361d5a78ed8cf360b55cbb" }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui28"] }
eframe = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }

162
komorebi-themes/src/lib.rs Normal file
View File

@@ -0,0 +1,162 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
pub use base16_egui_themes::Base16;
pub use catppuccin_egui;
pub use eframe::egui::Color32;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum Theme {
/// A theme from catppuccin-egui
Catppuccin {
name: Catppuccin,
accent: Option<CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
name: Base16,
accent: Option<Base16Value>,
},
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub enum Base16Value {
Base00,
Base01,
Base02,
Base03,
Base04,
Base05,
#[default]
Base06,
Base07,
Base08,
Base09,
Base0A,
Base0B,
Base0C,
Base0D,
Base0E,
Base0F,
}
impl Base16Value {
pub fn color32(&self, theme: Base16) -> Color32 {
match self {
Base16Value::Base00 => theme.base00(),
Base16Value::Base01 => theme.base01(),
Base16Value::Base02 => theme.base02(),
Base16Value::Base03 => theme.base03(),
Base16Value::Base04 => theme.base04(),
Base16Value::Base05 => theme.base05(),
Base16Value::Base06 => theme.base06(),
Base16Value::Base07 => theme.base07(),
Base16Value::Base08 => theme.base08(),
Base16Value::Base09 => theme.base09(),
Base16Value::Base0A => theme.base0a(),
Base16Value::Base0B => theme.base0b(),
Base16Value::Base0C => theme.base0c(),
Base16Value::Base0D => theme.base0d(),
Base16Value::Base0E => theme.base0e(),
Base16Value::Base0F => theme.base0f(),
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum Catppuccin {
Frappe,
Latte,
Macchiato,
Mocha,
}
impl Catppuccin {
pub fn as_theme(self) -> catppuccin_egui::Theme {
self.into()
}
}
impl From<Catppuccin> for catppuccin_egui::Theme {
fn from(val: Catppuccin) -> Self {
match val {
Catppuccin::Frappe => catppuccin_egui::FRAPPE,
Catppuccin::Latte => catppuccin_egui::LATTE,
Catppuccin::Macchiato => catppuccin_egui::MACCHIATO,
Catppuccin::Mocha => catppuccin_egui::MOCHA,
}
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub enum CatppuccinValue {
Rosewater,
Flamingo,
Pink,
Mauve,
Red,
Maroon,
Peach,
Yellow,
Green,
Teal,
Sky,
Sapphire,
Blue,
Lavender,
#[default]
Text,
Subtext1,
Subtext0,
Overlay2,
Overlay1,
Overlay0,
Surface2,
Surface1,
Surface0,
Base,
Mantle,
Crust,
}
pub fn color32_compat(rgba: [u8; 4]) -> Color32 {
Color32::from_rgba_unmultiplied(rgba[0], rgba[1], rgba[2], rgba[3])
}
impl CatppuccinValue {
pub fn color32(&self, theme: catppuccin_egui::Theme) -> Color32 {
match self {
CatppuccinValue::Rosewater => color32_compat(theme.rosewater.to_srgba_unmultiplied()),
CatppuccinValue::Flamingo => color32_compat(theme.flamingo.to_srgba_unmultiplied()),
CatppuccinValue::Pink => color32_compat(theme.pink.to_srgba_unmultiplied()),
CatppuccinValue::Mauve => color32_compat(theme.mauve.to_srgba_unmultiplied()),
CatppuccinValue::Red => color32_compat(theme.red.to_srgba_unmultiplied()),
CatppuccinValue::Maroon => color32_compat(theme.maroon.to_srgba_unmultiplied()),
CatppuccinValue::Peach => color32_compat(theme.peach.to_srgba_unmultiplied()),
CatppuccinValue::Yellow => color32_compat(theme.yellow.to_srgba_unmultiplied()),
CatppuccinValue::Green => color32_compat(theme.green.to_srgba_unmultiplied()),
CatppuccinValue::Teal => color32_compat(theme.teal.to_srgba_unmultiplied()),
CatppuccinValue::Sky => color32_compat(theme.sky.to_srgba_unmultiplied()),
CatppuccinValue::Sapphire => color32_compat(theme.sapphire.to_srgba_unmultiplied()),
CatppuccinValue::Blue => color32_compat(theme.blue.to_srgba_unmultiplied()),
CatppuccinValue::Lavender => color32_compat(theme.lavender.to_srgba_unmultiplied()),
CatppuccinValue::Text => color32_compat(theme.text.to_srgba_unmultiplied()),
CatppuccinValue::Subtext1 => color32_compat(theme.subtext1.to_srgba_unmultiplied()),
CatppuccinValue::Subtext0 => color32_compat(theme.subtext0.to_srgba_unmultiplied()),
CatppuccinValue::Overlay2 => color32_compat(theme.overlay2.to_srgba_unmultiplied()),
CatppuccinValue::Overlay1 => color32_compat(theme.overlay1.to_srgba_unmultiplied()),
CatppuccinValue::Overlay0 => color32_compat(theme.overlay0.to_srgba_unmultiplied()),
CatppuccinValue::Surface2 => color32_compat(theme.surface2.to_srgba_unmultiplied()),
CatppuccinValue::Surface1 => color32_compat(theme.surface1.to_srgba_unmultiplied()),
CatppuccinValue::Surface0 => color32_compat(theme.surface0.to_srgba_unmultiplied()),
CatppuccinValue::Base => color32_compat(theme.base.to_srgba_unmultiplied()),
CatppuccinValue::Mantle => color32_compat(theme.mantle.to_srgba_unmultiplied()),
CatppuccinValue::Crust => color32_compat(theme.crust.to_srgba_unmultiplied()),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.28-dev.0"
version = "0.1.30"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -11,44 +11,47 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-core = { path = "../komorebi-core" }
komorebi-themes = { path = "../komorebi-themes" }
bitflags = { version = "2", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
clap = { workspace = true }
color-eyre = { workspace = true }
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
crossbeam-channel = { workspace = true }
crossbeam-utils = { workspace = true }
ctrlc = { version = "3", features = ["termination"] }
dirs = { workspace = true }
dunce = { workspace = true }
getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
hotwatch = "0.5"
lazy_static = "1"
hotwatch = { workspace = true }
lazy_static = { workspace = true }
miow = "0.6"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.8"
parking_lot = "0.12"
paste = "1"
paste = { workspace = true }
regex = "1"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
shadow-rs = { workspace = true }
strum = { version = "0.26", features = ["derive"] }
sysinfo = { workspace = true }
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uds_windows = "1"
which = "6"
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
uds_windows = { workspace = true }
which = { workspace = true }
widestring = "1"
win32-display-data = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.52"
shadow-rs = { workspace = true }
[build-dependencies]
shadow-rs = { workspace = true }

View File

@@ -1,6 +1,6 @@
use crate::core::AnimationStyle;
use crate::core::Rect;
use color_eyre::Result;
use komorebi_core::AnimationStyle;
use komorebi_core::Rect;
use schemars::JsonSchema;
@@ -416,12 +416,15 @@ impl Animation {
pub fn new(hwnd: isize) -> Self {
Self { hwnd }
}
pub fn cancel(&mut self) {
/// Returns true if the animation needs to continue
pub fn cancel(&mut self) -> bool {
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
return;
return true;
}
ANIMATION_MANAGER.lock().cancel(self.hwnd);
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
let max_duration = Duration::from_secs(1);
let spent_duration = Instant::now();
@@ -434,6 +437,12 @@ impl Animation {
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
));
}
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
latest_cancel_idx == cancel_idx
}
#[allow(clippy::cast_possible_truncation)]
@@ -460,7 +469,11 @@ impl Animation {
mut render_callback: impl FnMut(f64) -> Result<()>,
) -> Result<()> {
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
self.cancel();
let should_animate = self.cancel();
if !should_animate {
return Ok(());
}
}
ANIMATION_MANAGER.lock().start(self.hwnd);
@@ -474,8 +487,7 @@ impl Animation {
// check if animation is cancelled
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
// cancel animation
// set all flags
ANIMATION_MANAGER.lock().end(self.hwnd);
ANIMATION_MANAGER.lock().cancel(self.hwnd);
return Ok(());
}
@@ -485,8 +497,10 @@ impl Animation {
render_callback(progress).ok();
// sleep until next frame
if frame_start.elapsed() < target_frame_time {
std::thread::sleep(target_frame_time - frame_start.elapsed());
let frame_time_elapsed = frame_start.elapsed();
if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}

View File

@@ -8,7 +8,8 @@ pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub is_cancelled: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}
#[derive(Debug)]
@@ -31,7 +32,7 @@ impl AnimationManager {
pub fn is_cancelled(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.is_cancelled
animation_state.pending_cancel_count > 0
} else {
false
}
@@ -45,9 +46,35 @@ impl AnimationManager {
}
}
pub fn init_cancel(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;
// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn latest_cancel_idx(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn end_cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count -= 1;
}
}
pub fn cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.is_cancelled = true;
animation_state.in_progress = false;
}
}
@@ -55,7 +82,8 @@ impl AnimationManager {
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
e.insert(AnimationState {
in_progress: true,
is_cancelled: false,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
@@ -70,10 +98,11 @@ impl AnimationManager {
pub fn end(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
animation_state.is_cancelled = false;
self.animations.remove(&hwnd);
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
if animation_state.pending_cancel_count == 0 {
self.animations.remove(&hwnd);
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
}
}
}
}

View File

@@ -5,11 +5,12 @@ use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUS_STATE;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::windows_api;
use crate::WindowsApi;
use crate::WINDOWS_11;
use komorebi_core::BorderStyle;
use komorebi_core::Rect;
use crate::core::BorderStyle;
use crate::core::Rect;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
@@ -46,10 +47,11 @@ use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
let hwnd = hwnd.0 as isize;
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
if class.starts_with("komoborder") {
hwnds.push(hwnd.0);
hwnds.push(hwnd);
}
}
@@ -69,7 +71,7 @@ impl From<isize> for Border {
impl Border {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
@@ -91,8 +93,9 @@ impl Border {
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let instance = h_module.0 as isize;
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), h_module)?;
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
hwnd_sender.send(hwnd)?;
let mut msg: MSG = MSG::default();
@@ -103,7 +106,8 @@ impl Border {
tracing::debug!("border window event processing thread shutdown");
break;
};
TranslateMessage(&msg);
// TODO: error handling
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
@@ -119,7 +123,7 @@ impl Border {
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::close_window(self.hwnd())
WindowsApi::close_window(self.hwnd)
}
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
@@ -129,8 +133,8 @@ impl Border {
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
// Update the position of the border if required
if !WindowsApi::window_rect(self.hwnd())?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd(), &rect, HWND((Z_ORDER.load()).into()))?;
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
should_invalidate = true;
}
@@ -159,13 +163,13 @@ impl Border {
let hdc = BeginPaint(window, &mut ps);
// With the rect that we set in Self::update
match WindowsApi::window_rect(window) {
match WindowsApi::window_rect(window.0 as isize) {
Ok(rect) => {
// Grab the focus kind for this border
let window_kind = {
FOCUS_STATE
.lock()
.get(&window.0)
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused)
};
@@ -191,27 +195,35 @@ impl Border {
match STYLE.load() {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
// TODO: error handling
let _ =
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
// TODO: error handling
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
BorderStyle::Rounded => {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
// TODO: error handling
let _ = RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
}
BorderStyle::Square => {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
// TODO: error handling
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
DeleteObject(hpen);
DeleteObject(hbrush);
// TODO: error handling
let _ = DeleteObject(hpen);
// TODO: error handling
let _ = DeleteObject(hbrush);
}
Err(error) => {
tracing::error!("could not get border rect: {}", error.to_string())
}
}
EndPaint(window, &ps);
// TODO: error handling
let _ = EndPaint(window, &ps);
LRESULT(0)
}
WM_DESTROY => {

View File

@@ -2,12 +2,21 @@
mod border;
use crate::core::BorderImplementation;
use crate::core::BorderStyle;
use crate::core::WindowKind;
use crate::ring::Ring;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::Colour;
use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
use border::Border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::BorderImplementation;
use komorebi_core::BorderStyle;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use schemars::JsonSchema;
@@ -21,17 +30,6 @@ use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
use crate::ring::Ring;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::Colour;
use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
use border::Border;
use komorebi_core::WindowKind;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
@@ -43,7 +41,7 @@ lazy_static! {
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
AtomicCell::new(BorderImplementation::Windows);
AtomicCell::new(BorderImplementation::Komorebi);
pub static ref FOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
pub static ref UNFOCUSED: AtomicU32 =
@@ -51,6 +49,8 @@ lazy_static! {
pub static ref MONOCLE: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
pub static ref FLOATING: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(245, 245, 165))));
}
lazy_static! {
@@ -59,7 +59,7 @@ lazy_static! {
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
}
pub struct Notification;
pub struct Notification(pub Option<isize>);
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
@@ -75,8 +75,8 @@ fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification() {
if event_tx().try_send(Notification).is_err() {
pub fn send_notification(hwnd: Option<isize>) {
if event_tx().try_send(Notification(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
@@ -120,6 +120,7 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
WindowKind::Floating => FLOATING.load(Ordering::SeqCst),
}
}
@@ -141,19 +142,29 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
let receiver = event_rx();
event_tx().send(Notification)?;
event_tx().send(Notification(None))?;
let mut previous_snapshot = Ring::default();
let mut previous_pending_move_op = None;
let mut previous_is_paused = false;
let mut previous_notification: Option<Notification> = None;
'receiver: for _ in receiver {
'receiver: for notification in receiver {
// Check the wm state every time we receive a notification
let state = wm.lock();
let is_paused = state.is_paused;
let focused_monitor_idx = state.focused_monitor_idx();
let focused_workspace_idx =
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
let monitors = state.monitors.clone();
let pending_move_op = state.pending_move_op;
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
[focused_workspace_idx]
.floating_windows()
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
drop(state);
match IMPLEMENTATION.load() {
@@ -222,6 +233,21 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
should_process_notification = true;
}
// when we switch focus to a floating window
if !should_process_notification
&& floating_window_hwnds.contains(&notification.0.unwrap_or_default())
{
should_process_notification = true;
}
if !should_process_notification {
if let Some(ref previous) = previous_notification {
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
should_process_notification = true;
}
}
}
if !should_process_notification {
tracing::trace!("monitor state matches latest snapshot, skipping notification");
continue 'receiver;
@@ -300,7 +326,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd(),
monocle.focused_window().copied().unwrap_or_default().hwnd,
)?;
border.update(&rect, true)?;
@@ -324,9 +350,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
continue 'monitors;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
let is_maximized = WindowsApi::is_zoomed(
WindowsApi::foreground_window().unwrap_or_default(),
));
);
if is_maximized {
let mut to_remove = vec![];
@@ -347,16 +373,20 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
// Destroy any borders not associated with the focused workspace
let container_ids = ws
let mut container_and_floating_window_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
for w in ws.floating_windows() {
container_and_floating_window_ids.push(w.hwnd.to_string());
}
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
&& !container_and_floating_window_ids.contains(id)
{
border.destroy()?;
to_remove.push(id.clone());
@@ -368,13 +398,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
for (idx, c) in ws.containers().iter().enumerate() {
let hwnd = c.focused_window().copied().unwrap_or_default().hwnd;
let notification_hwnd = notification.0.unwrap_or_default();
// Update border when moving or resizing with mouse
if pending_move_op.is_some() && idx == ws.focused_container_idx() {
if pending_move_op.is_some()
&& idx == ws.focused_container_idx()
&& hwnd == notification_hwnd
{
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
let mut rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
while WindowsApi::lbutton_is_pressed() {
@@ -390,7 +426,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
};
let new_rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
if rect != new_rect {
@@ -438,7 +474,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
let rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
let should_invalidate = match last_focus_state {
@@ -448,6 +484,101 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
border.update(&rect, should_invalidate)?;
}
{
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
'windows: for window in ws.floating_windows() {
let hwnd = window.hwnd;
let notification_hwnd = notification.0.unwrap_or_default();
if pending_move_op.is_some() && hwnd == notification_hwnd {
let mut rect = WindowsApi::window_rect(hwnd)?;
while WindowsApi::lbutton_is_pressed() {
let border = match borders.entry(hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) =
Border::create(&hwnd.to_string())
{
entry.insert(border)
} else {
continue 'monitors;
}
}
};
let new_rect = WindowsApi::window_rect(hwnd)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect, true)?;
}
}
Z_ORDER.store(restore_z_order);
continue 'monitors;
}
let border = match borders.entry(window.hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(&window.hwnd.to_string())
{
entry.insert(border)
} else {
continue 'monitors;
}
}
};
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
let mut should_destroy = false;
if let Some(notification_hwnd) = notification.0 {
if notification_hwnd != window.hwnd {
should_destroy = true;
}
}
if WindowsApi::foreground_window().unwrap_or_default()
!= window.hwnd
{
should_destroy = true;
}
if should_destroy {
border.destroy()?;
borders.remove(&window.hwnd.to_string());
borders_monitors.remove(&window.hwnd.to_string());
continue 'windows;
}
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = WindowKind::Floating;
{
let mut focus_state = FOCUS_STATE.lock();
last_focus_state =
focus_state.insert(border.hwnd, new_focus_state);
}
let rect = WindowsApi::window_rect(window.hwnd)?;
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => last_focus_state != new_focus_state,
};
border.update(&rect, should_invalidate)?;
}
Z_ORDER.store(restore_z_order);
}
}
}
}
@@ -456,6 +587,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
previous_snapshot = monitors;
previous_pending_move_op = pending_move_op;
previous_is_paused = is_paused;
previous_notification = Some(notification);
}
Ok(())

View File

@@ -1,4 +1,5 @@
use hex_color::HexColor;
use komorebi_themes::Color32;
use schemars::gen::SchemaGenerator;
use schemars::schema::InstanceType;
use schemars::schema::Schema;
@@ -28,6 +29,16 @@ impl From<u32> for Colour {
}
}
impl From<Color32> for Colour {
fn from(value: Color32) -> Self {
Colour::Rgb(Rgb::new(
value.r() as u32,
value.g() as u32,
value.b() as u32,
))
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Hex(HexColor);

View File

@@ -10,13 +10,13 @@ use interfaces::IServiceProvider;
use std::ffi::c_void;
use windows::core::Interface;
use windows::Win32::Foundation::HWND;
use windows::Win32::System::Com::CoCreateInstance;
use windows::Win32::System::Com::CoInitializeEx;
use windows::Win32::System::Com::CoUninitialize;
use windows::Win32::System::Com::CLSCTX_ALL;
use windows::Win32::System::Com::COINIT_APARTMENTTHREADED;
use windows_core::Interface;
struct ComInit();
@@ -77,7 +77,7 @@ pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
if view_collection.get_view_for_hwnd(hwnd, &mut view).is_err() {
tracing::error!(
"could not get view for hwnd {} due to os error: {}",
hwnd.0,
hwnd.0 as isize,
std::io::Error::last_os_error()
);
}
@@ -85,14 +85,14 @@ pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
view.map_or_else(
|| {
tracing::error!("no view was found for {}", hwnd.0,);
tracing::error!("no view was found for {}", hwnd.0 as isize);
},
|view| {
unsafe {
if view.set_cloak(cloak_type, flags).is_err() {
tracing::error!(
"could not change the cloaking status for hwnd {} due to os error: {}",
hwnd.0,
hwnd.0 as isize,
std::io::Error::last_os_error()
);
}

View File

@@ -9,7 +9,7 @@ use serde::Serialize;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
#[getset(get = "pub")]
id: String,
@@ -27,12 +27,6 @@ impl Default for Container {
}
}
impl PartialEq for Container {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Container {
pub fn hide(&self, omit: Option<isize>) {
for window in self.windows().iter().rev() {

View File

@@ -7,12 +7,12 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Rect;
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::CustomLayout;
use super::DefaultLayout;
use super::Rect;
pub trait Arrangement {
fn calculate(

135
komorebi/src/core/asc.rs Normal file
View File

@@ -0,0 +1,135 @@
use crate::config_generation::ApplicationConfiguration;
use crate::config_generation::ApplicationOptions;
use crate::config_generation::MatchingRule;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ApplicationSpecificConfiguration(pub BTreeMap<String, AscApplicationRulesOrSchema>);
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum AscApplicationRulesOrSchema {
AscApplicationRules(AscApplicationRules),
Schema(String),
}
impl Deref for ApplicationSpecificConfiguration {
type Target = BTreeMap<String, AscApplicationRulesOrSchema>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ApplicationSpecificConfiguration {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl ApplicationSpecificConfiguration {
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(pathbuf)?;
Ok(serde_json::from_str(&content)?)
}
pub fn format(pathbuf: &PathBuf) -> Result<String> {
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
}
}
/// Rules that determine how an application is handled
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct AscApplicationRules {
/// Rules to ignore specific windows
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<Vec<MatchingRule>>,
/// Rules to forcibly manage specific windows
#[serde(skip_serializing_if = "Option::is_none")]
pub manage: Option<Vec<MatchingRule>>,
/// Rules to manage specific windows as floating windows
#[serde(skip_serializing_if = "Option::is_none")]
pub floating: Option<Vec<MatchingRule>>,
/// Rules to ignore specific windows from the transparency feature
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency_ignore: Option<Vec<MatchingRule>>,
/// Rules to identify applications which minimize to the tray or have multiple windows
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_and_multi_window: Option<Vec<MatchingRule>>,
/// Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style
#[serde(skip_serializing_if = "Option::is_none")]
pub layered: Option<Vec<MatchingRule>>,
/// Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change: Option<Vec<MatchingRule>>,
/// Rules to identify applications which are slow to send initial event notifications
#[serde(skip_serializing_if = "Option::is_none")]
pub slow_application: Option<Vec<MatchingRule>>,
}
impl From<Vec<ApplicationConfiguration>> for ApplicationSpecificConfiguration {
fn from(value: Vec<ApplicationConfiguration>) -> Self {
let mut map = BTreeMap::new();
for entry in &value {
let key = entry.name.clone();
let mut rules = AscApplicationRules {
ignore: None,
manage: None,
floating: None,
transparency_ignore: None,
tray_and_multi_window: None,
layered: None,
object_name_change: None,
slow_application: None,
};
rules.ignore = entry.ignore_identifiers.clone();
if let Some(options) = &entry.options {
for opt in options {
match opt {
ApplicationOptions::ObjectNameChange => {
rules.object_name_change =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::Layered => {
rules.layered =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::TrayAndMultiWindow => {
rules.tray_and_multi_window =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::Force => {
rules.manage =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::BorderOverflow => {}
}
}
}
if rules.ignore.is_some()
|| rules.manage.is_some()
|| rules.floating.is_some()
|| rules.transparency_ignore.is_some()
|| rules.tray_and_multi_window.is_some()
|| rules.layered.is_some()
|| rules.object_name_change.is_some()
|| rules.slow_application.is_some()
{
map.insert(key, AscApplicationRulesOrSchema::AscApplicationRules(rules));
}
}
Self(map)
}
}

View File

@@ -6,7 +6,7 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::ApplicationIdentifier;
use super::ApplicationIdentifier;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
@@ -59,6 +59,14 @@ pub enum MatchingRule {
Composite(Vec<IdWithIdentifier>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceMatchingRule {
pub monitor_index: usize,
pub workspace_index: usize,
pub matching_rule: MatchingRule,
pub initial_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifier {
pub kind: ApplicationIdentifier,
@@ -108,7 +116,8 @@ pub struct ApplicationConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<ApplicationOptions>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub float_identifiers: Option<Vec<MatchingRule>>,
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Option<Vec<MatchingRule>>,
}
impl ApplicationConfiguration {
@@ -179,7 +188,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("# Generated by komorebic.exe"), String::new()];
let mut float_rules = vec![];
let mut ignore_rules = vec![];
for app in cfgen {
lines.push(format!("# {}", app.name));
@@ -193,15 +202,15 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_identifiers {
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule =
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("# {comment}"));
@@ -230,7 +239,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("; Generated by komorebic.exe"), String::new()];
let mut float_rules = vec![];
let mut ignore_rules = vec![];
for app in cfgen {
lines.push(format!("; {}", app.name));
@@ -244,8 +253,8 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_identifiers {
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
@@ -253,8 +262,8 @@ impl ApplicationConfigurationGenerator {
);
// 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 !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("; {comment}"));

View File

@@ -12,7 +12,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::Rect;
use super::Rect;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct CustomLayout(Vec<Column>);

View File

@@ -5,9 +5,9 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::OperationDirection;
use crate::Rect;
use crate::Sizing;
use super::OperationDirection;
use super::Rect;
use super::Sizing;
#[derive(
Clone,
@@ -35,6 +35,37 @@ pub enum DefaultLayout {
}
impl DefaultLayout {
pub fn leftmost_index(&self, len: usize) -> usize {
match self {
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
n if n > 1 => 1,
_ => 0,
},
DefaultLayout::BSP
| DefaultLayout::Columns
| DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::HorizontalStack
| DefaultLayout::Grid => 0,
}
}
pub fn rightmost_index(&self, len: usize) -> usize {
match self {
DefaultLayout::BSP
| DefaultLayout::Columns
| DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::HorizontalStack
| DefaultLayout::Grid => len.saturating_sub(1),
DefaultLayout::UltrawideVerticalStack => match len {
2 => 0,
_ => len.saturating_sub(1),
},
DefaultLayout::RightMainVerticalStack => 0,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
pub fn resize(
@@ -164,14 +195,14 @@ impl DefaultLayout {
#[must_use]
pub const fn cycle_previous(self) -> Self {
match self {
Self::BSP => Self::UltrawideVerticalStack,
Self::RightMainVerticalStack => Self::Grid,
Self::Grid => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::VerticalStack,
Self::VerticalStack => Self::Rows,
Self::Rows => Self::Columns,
Self::Columns => Self::Grid,
Self::Grid => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::BSP,
Self::Columns => Self::BSP,
Self::BSP => Self::RightMainVerticalStack,
}
}
}

View File

@@ -1,9 +1,9 @@
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::custom_layout::CustomLayout;
use crate::DefaultLayout;
use crate::OperationDirection;
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::custom_layout::CustomLayout;
use super::DefaultLayout;
use super::OperationDirection;
pub trait Direction {
fn index_in_direction(

View File

@@ -2,10 +2,10 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::Arrangement;
use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Direction;
use super::Arrangement;
use super::CustomLayout;
use super::DefaultLayout;
use super::Direction;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub enum Layout {

View File

@@ -27,6 +27,7 @@ pub use rect::Rect;
pub mod animation;
pub mod arrangement;
pub mod asc;
pub mod config_generation;
pub mod custom_layout;
pub mod cycle_direction;
@@ -45,12 +46,13 @@ pub enum SocketMessage {
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),
UnstackWindow,
CycleStack(CycleDirection),
FocusStackWindow(usize),
StackAll,
UnstackAll,
ResizeWindowEdge(OperationDirection, Sizing),
ResizeWindowAxis(Axis, Sizing),
UnstackWindow,
CycleStack(CycleDirection),
MoveContainerToMonitorNumber(usize),
CycleMoveContainerToMonitor(CycleDirection),
MoveContainerToWorkspaceNumber(usize),
@@ -76,6 +78,7 @@ pub enum SocketMessage {
ToggleMonocle,
ToggleMaximize,
ToggleWindowContainerBehaviour,
ToggleFloatOverride,
WindowHidingBehaviour(HidingBehaviour),
ToggleCrossMonitorMoveBehaviour,
CrossMonitorMoveBehaviour(MoveBehaviour),
@@ -89,6 +92,8 @@ pub enum SocketMessage {
CycleLayout(CycleDirection),
ChangeLayoutCustom(PathBuf),
FlipLayout(Axis),
ToggleWorkspaceWindowContainerBehaviour,
ToggleWorkspaceFloatOverride,
// Monitor and Workspace Commands
MonitorIndexPreference(usize, i32, i32, i32, i32),
DisplayIndexPreference(usize, String),
@@ -99,6 +104,7 @@ pub enum SocketMessage {
Stop,
TogglePause,
Retile,
RetileWithResizeDimensions,
QuickSave,
QuickLoad,
Save(PathBuf),
@@ -132,6 +138,7 @@ pub enum SocketMessage {
ClearNamedWorkspaceLayoutRules(String),
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
ReloadStaticConfiguration(PathBuf),
WatchConfiguration(bool),
CompleteConfiguration,
@@ -150,6 +157,7 @@ pub enum SocketMessage {
BorderOffset(i32),
BorderImplementation(BorderImplementation),
Transparency(bool),
ToggleTransparency,
TransparencyAlpha(u8),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
@@ -171,7 +179,8 @@ pub enum SocketMessage {
ClearWorkspaceRules(usize, usize),
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
FloatRule(ApplicationIdentifier, String),
#[serde(alias = "FloatRule")]
IgnoreRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
IdentifyTrayApplication(ApplicationIdentifier, String),
@@ -189,6 +198,7 @@ pub enum SocketMessage {
RemoveTitleBar(ApplicationIdentifier, String),
ToggleTitleBars,
AddSubscriberSocket(String),
AddSubscriberSocketWithOptions(String, SubscribeOptions),
RemoveSubscriberSocket(String),
AddSubscriberPipe(String),
RemoveSubscriberPipe(String),
@@ -214,6 +224,12 @@ impl FromStr for SocketMessage {
}
}
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SubscribeOptions {
/// Only emit notifications when the window manager state has changed
pub filter_state_changes: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
@@ -291,6 +307,7 @@ pub enum WindowKind {
Stack,
Monocle,
Unfocused,
Floating,
}
#[derive(
@@ -328,7 +345,16 @@ pub enum ApplicationIdentifier {
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Copy,
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
@@ -337,18 +363,48 @@ pub enum FocusFollowsMouseImplementation {
Windows,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct WindowManagementBehaviour {
/// The current WindowContainerBehaviour to be used
pub current_behaviour: WindowContainerBehaviour,
/// Override of `current_behaviour` to open new windows as floating windows
/// that can be later toggled to tiled, when false it will default to
/// `current_behaviour` again.
pub float_override: bool,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
Default,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
pub enum WindowContainerBehaviour {
/// Create a new container for each new window
#[default]
Create,
/// Append new windows to the focused window container
Append,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
@@ -359,6 +415,16 @@ pub enum MoveBehaviour {
NoOp,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum CrossBoundaryBehaviour {
/// Attempt to perform actions across a workspace boundary
Workspace,
/// Attempt to perform actions across a monitor boundary
Monitor,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
@@ -372,7 +438,16 @@ pub enum HidingBehaviour {
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows

View File

@@ -7,8 +7,8 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::direction::Direction;
use crate::Axis;
use super::direction::Direction;
use super::Axis;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,

View File

@@ -8,6 +8,7 @@ pub mod com;
pub mod ring;
pub mod colour;
pub mod container;
pub mod core;
pub mod focus_manager;
pub mod monitor;
pub mod monitor_reconciliator;
@@ -48,6 +49,7 @@ use std::sync::Arc;
pub use animation::*;
pub use animation_manager::*;
pub use colour::*;
pub use core::*;
pub use process_command::*;
pub use process_event::*;
pub use static_config::*;
@@ -57,15 +59,11 @@ pub use window_manager_event::*;
pub use windows_api::WindowsApi;
pub use windows_api::*;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::config_generation::WorkspaceMatchingRule;
use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::AnimationStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
use komorebi_core::SocketMessage;
use os_info::Version;
use parking_lot::Mutex;
use regex::Regex;
@@ -77,8 +75,6 @@ use which::which;
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
type WorkspaceRule = (usize, usize, bool);
lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
@@ -133,16 +129,17 @@ lazy_static! {
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
]));
static ref TRANSPARENCY_BLACKLIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref WORKSPACE_MATCHING_RULES: Arc<Mutex<Vec<WorkspaceMatchingRule>>> =
Arc::new(Mutex::new(Vec::new()));
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
static ref IGNORE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
// mstsc.exe creates these on Windows 11 when a WSL process is launched
// https://github.com/LGUG2Z/komorebi/issues/74
MatchingRule::Simple(IdWithIdentifier {
@@ -154,9 +151,14 @@ lazy_static! {
kind: ApplicationIdentifier::Class,
id: String::from("IHWindowClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("komorebi-bar.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
})
]));
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"Chrome_RenderWidgetHostHWND".to_string(),
]));
@@ -164,9 +166,18 @@ lazy_static! {
"X410.exe".to_string(),
"vcxsrv.exe".to_string(),
]));
static ref SLOW_APPLICATION_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
]));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref SUBSCRIPTION_SOCKET_OPTIONS: Arc<Mutex<HashMap<String, SubscribeOptions>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -216,7 +227,6 @@ lazy_static! {
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
@@ -228,9 +238,10 @@ pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_TEMPORARY_DISABLED: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
@@ -288,18 +299,34 @@ pub struct Notification {
pub state: State,
}
pub fn notify_subscribers(notification: &str) -> Result<()> {
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
let is_subscription_event = matches!(
notification.event,
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
);
let notification = &serde_json::to_string(&notification)?;
let mut stale_sockets = vec![];
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
let options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
for (socket, path) in &mut *sockets {
match UnixStream::connect(path) {
Ok(mut stream) => {
tracing::debug!("pushed notification to subscriber: {socket}");
stream.write_all(notification.as_bytes())?;
}
Err(_) => {
stale_sockets.push(socket.clone());
let apply_state_filter = (*options)
.get(socket)
.copied()
.unwrap_or_default()
.filter_state_changes;
if !apply_state_filter || state_has_been_modified || is_subscription_event {
match UnixStream::connect(path) {
Ok(mut stream) => {
tracing::debug!("pushed notification to subscriber: {socket}");
stream.write_all(notification.as_bytes())?;
}
Err(_) => {
stale_sockets.push(socket.clone());
}
}
}
}
@@ -307,6 +334,13 @@ pub fn notify_subscribers(notification: &str) -> Result<()> {
for socket in stale_sockets {
tracing::warn!("removing stale subscription: {socket}");
sockets.remove(&socket);
let socket_path = DATA_DIR.join(socket);
if let Err(error) = std::fs::remove_file(&socket_path) {
tracing::error!(
"could not remove stale subscriber socket file at {}: {error}",
socket_path.display()
)
}
}
let mut stale_pipes = vec![];

View File

@@ -7,6 +7,7 @@
clippy::doc_markdown
)]
use std::net::Shutdown;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::sync::Arc;
@@ -20,9 +21,11 @@ use crossbeam_utils::Backoff;
use parking_lot::deadlock;
use parking_lot::Mutex;
use sysinfo::Process;
use sysinfo::ProcessesToUpdate;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use uds_windows::UnixStream;
use komorebi::border_manager;
use komorebi::focus_manager;
@@ -165,9 +168,9 @@ fn main() -> Result<()> {
SESSION_ID.store(session_id, Ordering::SeqCst);
let mut system = sysinfo::System::new_all();
system.refresh_processes();
system.refresh_processes(ProcessesToUpdate::All);
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe").collect();
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
if matched_procs.len() > 1 {
let mut len = matched_procs.len();
@@ -218,6 +221,7 @@ fn main() -> Result<()> {
Arc::new(Mutex::new(StaticConfig::preload(
config,
winevent_listener::event_rx(),
None,
)?))
} else {
Arc::new(Mutex::new(WindowManager::new(
@@ -287,5 +291,15 @@ fn main() -> Result<()> {
WindowsApi::disable_focus_follows_mouse()?;
}
let sockets = komorebi::SUBSCRIPTION_SOCKETS.lock();
for path in (*sockets).values() {
if let Ok(stream) = UnixStream::connect(path) {
stream.shutdown(Shutdown::Both)?;
}
}
let socket = DATA_DIR.join("komorebi.sock");
let _ = std::fs::remove_file(socket);
std::process::exit(130);
}

View File

@@ -12,11 +12,15 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Rect;
use crate::core::Rect;
use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
use crate::DefaultLayout;
use crate::Layout;
use crate::OperationDirection;
use crate::WindowsApi;
#[derive(
Debug,
@@ -87,6 +91,22 @@ pub fn new(
}
impl Monitor {
pub fn placeholder() -> Self {
Self {
id: 0,
name: "PLACEHOLDER".to_string(),
device: "".to_string(),
device_id: "".to_string(),
size: Default::default(),
work_area_size: Default::default(),
work_area_offset: None,
window_based_work_area_offset: None,
window_based_work_area_offset_limit: 0,
workspaces: Default::default(),
last_focused_workspace: None,
workspace_names: Default::default(),
}
}
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
let focused_idx = self.focused_workspace_idx();
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
@@ -114,7 +134,7 @@ impl Monitor {
.ok_or_else(|| anyhow!("there is no workspace"))?
};
workspace.add_container(container);
workspace.add_container_to_back(container);
Ok(())
}
@@ -149,6 +169,7 @@ impl Monitor {
&mut self,
target_workspace_idx: usize,
follow: bool,
direction: Option<OperationDirection>,
) -> Result<()> {
let workspace = self
.focused_workspace_mut()
@@ -158,22 +179,92 @@ impl Monitor {
bail!("cannot move native maximized window to another monitor or workspace");
}
let container = workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
let foreground_hwnd = WindowsApi::foreground_window()?;
let floating_window_index = workspace
.floating_windows()
.iter()
.position(|w| w.hwnd == foreground_hwnd);
let workspaces = self.workspaces_mut();
if let Some(idx) = floating_window_index {
let window = workspace.floating_windows_mut().remove(idx);
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
let workspaces = self.workspaces_mut();
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
Some(workspace) => workspace,
};
target_workspace.floating_windows_mut().push(window);
} else {
let container = workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
let workspaces = self.workspaces_mut();
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
Some(workspace) => workspace,
};
match direction {
Some(OperationDirection::Left) => match target_workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
target_workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.insert_container_at_idx(0, container);
} else {
target_workspace.add_container_to_back(container);
}
}
_ => {
target_workspace.add_container_to_back(container);
}
},
Layout::Custom(_) => {
target_workspace.add_container_to_back(container);
}
},
Some(OperationDirection::Right) => match target_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(target_workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.add_container_to_back(container);
} else {
target_workspace
.insert_container_at_idx(target_index, container);
}
}
_ => {
target_workspace.insert_container_at_idx(target_index, container);
}
}
}
Layout::Custom(_) => {
target_workspace.add_container_to_front(container);
}
},
_ => {
target_workspace.add_container_to_back(container);
}
}
Some(workspace) => workspace,
};
target_workspace.add_container(container);
}
if follow {
self.focus_workspace(target_workspace_idx)?;

View File

@@ -28,6 +28,7 @@ use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
use crate::monitor_reconciliator;
use crate::windows_api;
use crate::WindowsApi;
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
@@ -44,7 +45,7 @@ impl From<isize> for Hidden {
impl Hidden {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn create(name: &str) -> color_eyre::Result<Self> {
@@ -65,8 +66,9 @@ impl Hidden {
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let instance = h_module.0 as isize;
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name.as_ptr()), h_module)?;
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name.as_ptr()), instance)?;
hwnd_sender.send(hwnd)?;
let mut msg: MSG = MSG::default();
@@ -77,7 +79,8 @@ impl Hidden {
tracing::debug!("hidden window event processing thread shutdown");
break;
};
TranslateMessage(&msg);
// TODO: error handling
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}

View File

@@ -1,6 +1,7 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::core::Rect;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
@@ -10,7 +11,6 @@ use crate::WindowsApi;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::Rect;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
@@ -67,11 +67,17 @@ pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
.flatten()
.map(|display| {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let (device, device_id) = if path.is_empty() {
(String::from("UNKNOWN"), String::from("UNKNOWN"))
} else {
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
(device, device_id)
};
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
@@ -166,7 +172,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if should_update {
tracing::info!("updated work area for {}", monitor.device_id());
monitor.update_focused_workspace(offset)?;
border_manager::send_notification();
border_manager::send_notification(None);
} else {
tracing::debug!(
"work areas match, reconciliation not required for {}",
@@ -213,7 +219,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
);
monitor.update_focused_workspace(offset)?;
border_manager::send_notification();
border_manager::send_notification(None);
} else {
tracing::debug!(
"resolutions match, reconciliation not required for {}",
@@ -313,7 +319,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Put the orphaned containers somewhere visible
for container in orphaned_containers {
focused_ws.add_container(container);
focused_ws.add_container_to_back(container);
}
// Gotta reset the focus or the movement will feel "off"
@@ -400,7 +406,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Second retile to fix DPI/resolution related jank
wm.retile_all(true)?;
// Border updates to fix DPI/resolution related jank
border_manager::send_notification();
border_manager::send_notification(None);
}
}
}

View File

@@ -4,6 +4,7 @@ use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::net::Shutdown;
use std::net::TcpListener;
use std::net::TcpStream;
use std::num::NonZeroUsize;
@@ -21,28 +22,29 @@ use schemars::gen::SchemaSettings;
use schemars::schema_for;
use uds_windows::UnixStream;
use komorebi_core::config_generation::ApplicationConfiguration;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
use komorebi_core::BorderImplementation;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::Layout;
use komorebi_core::MoveBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;
use komorebi_core::WindowContainerBehaviour;
use komorebi_core::WindowKind;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::ApplicationIdentifier;
use crate::core::Axis;
use crate::core::BorderImplementation;
use crate::core::FocusFollowsMouseImplementation;
use crate::core::Layout;
use crate::core::MoveBehaviour;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::core::Sizing;
use crate::core::SocketMessage;
use crate::core::StateQuery;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowKind;
use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::colour::Rgb;
use crate::config_generation::WorkspaceMatchingRule;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::stackbar_manager;
@@ -55,9 +57,11 @@ use crate::window::Window;
use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
@@ -65,8 +69,8 @@ use crate::ANIMATION_STYLE;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
@@ -76,10 +80,11 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
use crate::TCP_CONNECTIONS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
use crate::WORKSPACE_RULES;
use crate::WORKSPACE_MATCHING_RULES;
use stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use stackbar_manager::STACKBAR_LABEL;
use stackbar_manager::STACKBAR_MODE;
@@ -184,6 +189,10 @@ impl WindowManager {
}
}
#[allow(clippy::useless_asref)]
// We don't have From implemented for &mut WindowManager
let initial_state = State::from(self.as_ref());
match message {
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
if let Some(monitor) = self.focused_monitor_mut() {
@@ -228,9 +237,20 @@ impl WindowManager {
self.cycle_container_window_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::FocusStackWindow(idx) => {
// In case you are using this command on a bar on a monitor
// different from the currently focused one, you'd want that
// monitor to be focused so that the FocusStackWindow happens
// on the monitor with the bar you just pressed.
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
}
self.focus_container_window(idx)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::ForceFocus => {
let focused_window = self.focused_window()?;
let focused_window_rect = WindowsApi::window_rect(focused_window.hwnd())?;
let focused_window_rect = WindowsApi::window_rect(focused_window.hwnd)?;
WindowsApi::center_cursor_in_rect(&focused_window_rect)?;
WindowsApi::left_click();
}
@@ -263,58 +283,101 @@ impl WindowManager {
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
}
}
SocketMessage::InitialWorkspaceRule(_, ref id, monitor_idx, workspace_idx) => {
self.handle_initial_workspace_rules(id, monitor_idx, workspace_idx)?;
}
SocketMessage::InitialNamedWorkspaceRule(_, ref id, ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
self.handle_initial_workspace_rules(id, monitor_idx, workspace_idx)?;
SocketMessage::InitialWorkspaceRule(identifier, ref id, monitor_idx, workspace_idx) => {
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
let workspace_matching_rule = WorkspaceMatchingRule {
monitor_index: monitor_idx,
workspace_index: workspace_idx,
matching_rule: MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.to_string(),
matching_strategy: Some(MatchingStrategy::Legacy),
}),
initial_only: true,
};
if !workspace_rules.contains(&workspace_matching_rule) {
workspace_rules.push(workspace_matching_rule);
}
}
SocketMessage::WorkspaceRule(_, ref id, monitor_idx, workspace_idx) => {
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
}
SocketMessage::NamedWorkspaceRule(_, ref id, ref workspace) => {
SocketMessage::InitialNamedWorkspaceRule(identifier, ref id, ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
let workspace_matching_rule = WorkspaceMatchingRule {
monitor_index: monitor_idx,
workspace_index: workspace_idx,
matching_rule: MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.to_string(),
matching_strategy: Some(MatchingStrategy::Legacy),
}),
initial_only: true,
};
if !workspace_rules.contains(&workspace_matching_rule) {
workspace_rules.push(workspace_matching_rule);
}
}
}
SocketMessage::WorkspaceRule(identifier, ref id, monitor_idx, workspace_idx) => {
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
let workspace_matching_rule = WorkspaceMatchingRule {
monitor_index: monitor_idx,
workspace_index: workspace_idx,
matching_rule: MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.to_string(),
matching_strategy: Some(MatchingStrategy::Legacy),
}),
initial_only: false,
};
if !workspace_rules.contains(&workspace_matching_rule) {
workspace_rules.push(workspace_matching_rule);
}
}
SocketMessage::NamedWorkspaceRule(identifier, ref id, ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
let workspace_matching_rule = WorkspaceMatchingRule {
monitor_index: monitor_idx,
workspace_index: workspace_idx,
matching_rule: MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.to_string(),
matching_strategy: Some(MatchingStrategy::Legacy),
}),
initial_only: false,
};
if !workspace_rules.contains(&workspace_matching_rule) {
workspace_rules.push(workspace_matching_rule);
}
}
}
SocketMessage::ClearWorkspaceRules(monitor_idx, workspace_idx) => {
let mut workspace_rules = WORKSPACE_RULES.lock();
let mut to_remove = vec![];
for (id, (m_idx, w_idx, _)) in workspace_rules.iter() {
if monitor_idx == *m_idx && workspace_idx == *w_idx {
to_remove.push(id.clone());
}
}
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
for rule in to_remove {
workspace_rules.remove(&rule);
}
workspace_rules.retain(|r| {
r.monitor_index != monitor_idx && r.workspace_index != workspace_idx
});
}
SocketMessage::ClearNamedWorkspaceRules(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
let mut workspace_rules = WORKSPACE_RULES.lock();
let mut to_remove = vec![];
for (id, (m_idx, w_idx, _)) in workspace_rules.iter() {
if monitor_idx == *m_idx && workspace_idx == *w_idx {
to_remove.push(id.clone());
}
}
for rule in to_remove {
workspace_rules.remove(&rule);
}
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_rules.retain(|r| {
r.monitor_index != monitor_idx && r.workspace_index != workspace_idx
});
}
}
SocketMessage::ClearAllWorkspaceRules => {
let mut workspace_rules = WORKSPACE_RULES.lock();
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_rules.clear();
}
SocketMessage::ManageRule(identifier, ref id) => {
@@ -337,20 +400,20 @@ impl WindowManager {
}));
}
}
SocketMessage::FloatRule(identifier, ref id) => {
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
SocketMessage::IgnoreRule(identifier, ref id) => {
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let mut should_push = true;
for f in &*float_identifiers {
if let MatchingRule::Simple(f) = f {
if f.id.eq(id) {
for i in &*ignore_identifiers {
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
ignore_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
@@ -436,7 +499,7 @@ impl WindowManager {
self.adjust_workspace_padding(sizing, adjustment)?;
}
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, true)?;
self.move_container_to_workspace(workspace_idx, true, None)?;
}
SocketMessage::CycleMoveContainerToWorkspace(direction) => {
let focused_monitor = self
@@ -452,7 +515,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
);
self.move_container_to_workspace(workspace_idx, true)?;
self.move_container_to_workspace(workspace_idx, true, None)?;
}
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, true)?;
@@ -470,7 +533,7 @@ impl WindowManager {
self.move_container_to_monitor(monitor_idx, None, true)?;
}
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, false)?;
self.move_container_to_workspace(workspace_idx, false, None)?;
}
SocketMessage::CycleSendContainerToWorkspace(direction) => {
let focused_monitor = self
@@ -486,7 +549,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
);
self.move_container_to_workspace(workspace_idx, false)?;
self.move_container_to_workspace(workspace_idx, false, None)?;
}
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, false)?;
@@ -569,6 +632,11 @@ impl WindowManager {
border_manager::destroy_all_borders()?;
self.retile_all(false)?
}
SocketMessage::RetileWithResizeDimensions => {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
border_manager::destroy_all_borders()?;
self.retile_all(true)?
}
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
@@ -775,6 +843,16 @@ impl WindowManager {
WindowsApi::disable_focus_follows_mouse()?;
}
let sockets = SUBSCRIPTION_SOCKETS.lock();
for path in (*sockets).values() {
if let Ok(stream) = UnixStream::connect(path) {
stream.shutdown(Shutdown::Both)?;
}
}
let socket = DATA_DIR.join("komorebi.sock");
let _ = std::fs::remove_file(socket);
std::process::exit(0)
}
SocketMessage::MonitorIndexPreference(index_preference, left, top, right, bottom) => {
@@ -1082,6 +1160,33 @@ impl WindowManager {
SocketMessage::ReloadConfiguration => {
Self::reload_configuration();
}
SocketMessage::ReplaceConfiguration(ref config) => {
// Check that this is a valid static config file first
if StaticConfig::read(config).is_ok() {
// Clear workspace rules; these will need to be replaced
WORKSPACE_MATCHING_RULES.lock().clear();
// Pause so that restored windows come to the foreground from all workspaces
self.is_paused = true;
// Bring all windows to the foreground
self.restore_all_windows()?;
// Create a new wm from the config path
let mut wm = StaticConfig::preload(
config,
winevent_listener::event_rx(),
self.command_listener.try_clone().ok(),
)?;
// Initialize the new wm
wm.init()?;
// This is equivalent to StaticConfig::postload for this use case
StaticConfig::reload(config, &mut wm)?;
// Set self to the new wm instance
*self = wm;
}
}
SocketMessage::ReloadStaticConfiguration(ref pathbuf) => {
self.reload_static_configuration(pathbuf)?;
}
@@ -1225,6 +1330,14 @@ impl WindowManager {
let socket_path = DATA_DIR.join(socket);
sockets.insert(socket.clone(), socket_path);
}
SocketMessage::AddSubscriberSocketWithOptions(ref socket, options) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
let socket_path = DATA_DIR.join(socket);
sockets.insert(socket.clone(), socket_path);
let mut socket_options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
socket_options.insert(socket.clone(), options);
}
SocketMessage::RemoveSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
sockets.remove(socket);
@@ -1252,15 +1365,52 @@ impl WindowManager {
self.resize_delta = delta;
}
SocketMessage::ToggleWindowContainerBehaviour => {
match self.window_container_behaviour {
match self.window_management_behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
self.window_container_behaviour = WindowContainerBehaviour::Append;
self.window_management_behaviour.current_behaviour =
WindowContainerBehaviour::Append;
}
WindowContainerBehaviour::Append => {
self.window_container_behaviour = WindowContainerBehaviour::Create;
self.window_management_behaviour.current_behaviour =
WindowContainerBehaviour::Create;
}
}
}
SocketMessage::ToggleFloatOverride => {
self.window_management_behaviour.float_override =
!self.window_management_behaviour.float_override;
}
SocketMessage::ToggleWorkspaceWindowContainerBehaviour => {
let current_global_behaviour = self.window_management_behaviour.current_behaviour;
if let Some(behaviour) = self
.focused_workspace_mut()?
.window_container_behaviour_mut()
{
match behaviour {
WindowContainerBehaviour::Create => {
*behaviour = WindowContainerBehaviour::Append
}
WindowContainerBehaviour::Append => {
*behaviour = WindowContainerBehaviour::Create
}
}
} else {
self.focused_workspace_mut()?
.set_window_container_behaviour(Some(match current_global_behaviour {
WindowContainerBehaviour::Create => WindowContainerBehaviour::Append,
WindowContainerBehaviour::Append => WindowContainerBehaviour::Create,
}));
};
}
SocketMessage::ToggleWorkspaceFloatOverride => {
let current_global_override = self.window_management_behaviour.float_override;
if let Some(float_override) = self.focused_workspace_mut()?.float_override_mut() {
*float_override = !*float_override;
} else {
self.focused_workspace_mut()?
.set_float_override(Some(!current_global_override));
};
}
SocketMessage::WindowHidingBehaviour(behaviour) => {
let mut hiding_behaviour = HIDING_BEHAVIOUR.lock();
*hiding_behaviour = behaviour;
@@ -1301,7 +1451,7 @@ impl WindowManager {
}
}
border_manager::send_notification();
border_manager::send_notification(None);
}
}
SocketMessage::BorderColour(kind, r, g, b) => match kind {
@@ -1317,6 +1467,9 @@ impl WindowManager {
WindowKind::Unfocused => {
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Floating => {
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
},
SocketMessage::BorderStyle(style) => {
STYLE.store(style);
@@ -1339,6 +1492,10 @@ impl WindowManager {
SocketMessage::AnimationStyle(style) => {
*ANIMATION_STYLE.lock() = style;
}
SocketMessage::ToggleTransparency => {
let current = transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED.store(!current, Ordering::SeqCst);
}
SocketMessage::Transparency(enable) => {
transparency_manager::TRANSPARENCY_ENABLED.store(enable, Ordering::SeqCst);
}
@@ -1436,64 +1593,21 @@ impl WindowManager {
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
};
let notification = Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
};
notify_subscribers(
Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
},
initial_state.has_been_modified(self.as_ref()),
)?;
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification();
border_manager::send_notification(None);
transparency_manager::send_notification();
stackbar_manager::send_notification();
tracing::info!("processed");
Ok(())
}
#[tracing::instrument(skip(self), level = "debug")]
fn handle_initial_workspace_rules(
&mut self,
id: &String,
monitor_idx: usize,
workspace_idx: usize,
) -> Result<()> {
self.handle_workspace_rules(id, monitor_idx, workspace_idx, true)?;
Ok(())
}
#[tracing::instrument(skip(self), level = "debug")]
fn handle_definitive_workspace_rules(
&mut self,
id: &String,
monitor_idx: usize,
workspace_idx: usize,
) -> Result<()> {
self.handle_workspace_rules(id, monitor_idx, workspace_idx, false)?;
Ok(())
}
#[tracing::instrument(skip(self), level = "debug")]
pub fn handle_workspace_rules(
&mut self,
id: &String,
monitor_idx: usize,
workspace_idx: usize,
initial_workspace_rule: bool,
) -> Result<()> {
{
let mut workspace_rules = WORKSPACE_RULES.lock();
workspace_rules.insert(
id.to_string(),
(monitor_idx, workspace_idx, initial_workspace_rule),
);
}
self.enforce_workspace_rules()?;
Ok(())
}
}
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream) -> Result<()> {
@@ -1505,22 +1619,29 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
for line in reader.lines() {
let message = SocketMessage::from_str(&line?)?;
let mut wm = wm.lock();
if wm.is_paused {
return match message {
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())
match wm.try_lock_for(Duration::from_secs(1)) {
None => {
tracing::warn!(
"could not acquire window manager lock, not processing message: {message}"
);
}
Some(mut wm) => {
if wm.is_paused {
return match message {
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())
}
};
}
};
}
wm.process_command(message.clone(), &mut stream)?;
wm.process_command(message.clone(), &mut stream)?;
}
}
}
Ok(())

View File

@@ -9,10 +9,10 @@ use color_eyre::Result;
use crossbeam_utils::atomic::AtomicConsume;
use parking_lot::Mutex;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::WindowContainerBehaviour;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::core::Sizing;
use crate::core::WindowContainerBehaviour;
use crate::border_manager;
use crate::border_manager::BORDER_OFFSET;
@@ -32,7 +32,9 @@ use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::DATA_DIR;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
@@ -63,7 +65,7 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
impl WindowManager {
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self, event), fields(event = event.title(), winevent = event.winevent(), hwnd = event.hwnd()))]
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
if self.is_paused {
tracing::trace!("ignoring while paused");
@@ -101,6 +103,10 @@ impl WindowManager {
}
if !transparency_override {
if rule_debug.matches_ignore_identifier.is_some() {
border_manager::send_notification(Option::from(event.hwnd()));
}
return Ok(());
}
}
@@ -117,6 +123,10 @@ impl WindowManager {
}
}
#[allow(clippy::useless_asref)]
// We don't have From implemented for &mut WindowManager
let initial_state = State::from(self.as_ref());
// Make sure we have the most recently focused monitor from any event
match event {
WindowManagerEvent::FocusChange(_, window)
@@ -149,14 +159,6 @@ impl WindowManager {
_ => {}
}
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
if let WindowManagerEvent::FocusChange(_, window) = event {
let _ = workspace.focus_changed(window.hwnd);
}
}
}
self.enforce_workspace_rules()?;
if matches!(event, WindowManagerEvent::MouseCapture(..)) {
@@ -246,24 +248,31 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, false)?;
let workspace = self.focused_workspace_mut()?;
if !workspace
let floating_window_idx = workspace
.floating_windows()
.iter()
.any(|w| w.hwnd == window.hwnd)
{
if let Some(w) = workspace.maximized_window() {
if w.hwnd == window.hwnd {
return Ok(());
.position(|w| w.hwnd == window.hwnd);
match floating_window_idx {
None => {
if let Some(w) = workspace.maximized_window() {
if w.hwnd == window.hwnd {
return Ok(());
}
}
if let Some(monocle) = workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(false)?;
}
} else {
workspace.focus_container_by_window(window.hwnd)?;
}
}
if let Some(monocle) = workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
Some(idx) => {
if let Some(window) = workspace.floating_windows().get(idx) {
window.focus(false)?;
}
} else {
self.focused_workspace_mut()?
.focus_container_by_window(window.hwnd)?;
}
}
}
@@ -329,26 +338,53 @@ impl WindowManager {
}
if proceed {
let behaviour =
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
let mut behaviour = self
.window_management_behaviour(focused_monitor_idx, focused_workspace_idx);
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && !needs_reconciliation {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
let floating_applications = FLOATING_APPLICATIONS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut should_float = false;
stackbar_manager::send_notification();
if !floating_applications.is_empty() {
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
(window.title(), window.exe(), window.class(), window.path())
{
should_float = should_act(
&title,
&exe_name,
&class,
&path,
&floating_applications,
&regex_identifiers,
)
.is_some();
}
}
behaviour.float_override = behaviour.float_override
|| (should_float && !matches!(event, WindowManagerEvent::Manage(_)));
if behaviour.float_override {
workspace.floating_windows_mut().push(window);
self.update_focused_workspace(false, false)?;
} else {
match behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
stackbar_manager::send_notification();
}
}
}
}
@@ -383,7 +419,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
.focused_container_idx();
WindowsApi::bring_window_to_top(window.hwnd())?;
WindowsApi::bring_window_to_top(window.hwnd)?;
self.pending_move_op =
Option::from((monitor_idx, workspace_idx, container_idx));
@@ -402,12 +438,12 @@ impl WindowManager {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
let window_container_behaviour =
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
let window_management_behaviour =
self.window_management_behaviour(focused_monitor_idx, focused_workspace_idx);
let workspace = self.focused_workspace_mut()?;
let focused_container_idx = workspace.focused_container_idx();
let new_position = WindowsApi::window_rect(window.hwnd())?;
let new_position = WindowsApi::window_rect(window.hwnd)?;
let old_position = *workspace
.latest_layout()
.get(focused_container_idx)
@@ -521,8 +557,11 @@ impl WindowManager {
}
// Here we handle a simple move on the same monitor which is treated as
// a container swap
} else if window_management_behaviour.float_override {
workspace.floating_windows_mut().push(window);
self.update_focused_workspace(false, false)?;
} else {
match window_container_behaviour {
match window_management_behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
@@ -581,14 +620,19 @@ impl WindowManager {
ops.push(resize_op!(resize.top, >, OperationDirection::Up));
}
// TODO: Determine if this is still needed
let top_left_constant = BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst);
if resize.right != 0 && resize.left == top_left_constant {
if resize.right != 0
&& (resize.left == top_left_constant || resize.left == 0)
{
ops.push(resize_op!(resize.right, <, OperationDirection::Right));
}
if resize.bottom != 0 && resize.top == top_left_constant {
if resize.bottom != 0
&& (resize.top == top_left_constant || resize.top == 0)
{
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
}
@@ -631,13 +675,15 @@ impl WindowManager {
serde_json::to_writer_pretty(&file, &known_hwnds)?;
let notification = Notification {
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
};
notify_subscribers(
Notification {
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
},
initial_state.has_been_modified(self.as_ref()),
)?;
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification();
border_manager::send_notification(Some(event.hwnd()));
transparency_manager::send_notification();
stackbar_manager::send_notification();

View File

@@ -5,7 +5,7 @@ use winput::message_loop;
use winput::message_loop::Event;
use winput::Action;
use komorebi_core::FocusFollowsMouseImplementation;
use crate::core::FocusFollowsMouseImplementation;
use crate::window_manager::WindowManager;

View File

@@ -51,7 +51,7 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
border_manager::send_notification();
border_manager::send_notification(None);
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,

View File

@@ -1,6 +1,8 @@
mod stackbar;
use crate::container::Container;
use crate::core::StackbarLabel;
use crate::core::StackbarMode;
use crate::stackbar_manager::stackbar::Stackbar;
use crate::WindowManager;
use crate::WindowsApi;
@@ -9,8 +11,6 @@ use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::hash_map::Entry;
@@ -21,7 +21,6 @@ use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
pub static STACKBAR_FONT_SIZE: AtomicI32 = AtomicI32::new(0); // 0 will produce the system default
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
@@ -128,9 +127,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
continue 'receiver;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
let is_maximized =
WindowsApi::is_zoomed(WindowsApi::foreground_window().unwrap_or_default());
// Handle the monocle container separately
if ws.monocle_container().is_some() || is_maximized {
@@ -207,11 +205,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
stackbars_monitors.insert(container.id().clone(), monitor_idx);
let rect = WindowsApi::window_rect(
container
.focused_window()
.copied()
.unwrap_or_default()
.hwnd(),
container.focused_window().copied().unwrap_or_default().hwnd,
)?;
stackbar.update(container_padding, container, &rect)?;

View File

@@ -2,6 +2,9 @@ use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::STYLE;
use crate::container::Container;
use crate::core::BorderStyle;
use crate::core::Rect;
use crate::core::StackbarLabel;
use crate::stackbar_manager::STACKBARS_CONTAINERS;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
@@ -11,19 +14,18 @@ use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::windows_api;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::WINDOWS_11;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::BorderStyle;
use komorebi_core::Rect;
use komorebi_core::StackbarLabel;
use std::os::windows::ffi::OsStrExt;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HINSTANCE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
@@ -84,7 +86,7 @@ impl From<isize> for Stackbar {
impl Stackbar {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
@@ -107,6 +109,7 @@ impl Stackbar {
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let name_cl = name.clone();
let instance = h_module.0 as isize;
std::thread::spawn(move || -> color_eyre::Result<()> {
unsafe {
let hwnd = CreateWindowExW(
@@ -120,12 +123,12 @@ impl Stackbar {
0,
None,
None,
h_module,
HINSTANCE(windows_api::as_ptr!(instance)),
None,
);
)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd_sender.send(hwnd)?;
hwnd_sender.send(hwnd.0 as isize)?;
let mut msg: MSG = MSG::default();
@@ -134,7 +137,8 @@ impl Stackbar {
tracing::debug!("stackbar window event processing thread shutdown");
break;
};
TranslateMessage(&msg);
// TODO: error handling
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
std::thread::sleep(Duration::from_millis(10))
@@ -145,12 +149,12 @@ impl Stackbar {
});
Ok(Self {
hwnd: hwnd_receiver.recv()?.0,
hwnd: hwnd_receiver.recv()?,
})
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::close_window(self.hwnd())
WindowsApi::close_window(self.hwnd)
}
pub fn update(
@@ -176,7 +180,7 @@ impl Stackbar {
layout.top -= workspace_specific_offset + STACKBAR_TAB_HEIGHT.load_consume();
layout.left -= workspace_specific_offset;
WindowsApi::position_window(self.hwnd(), &layout, false)?;
WindowsApi::position_window(self.hwnd, &layout, false)?;
unsafe {
let hdc = GetDC(self.hwnd());
@@ -232,16 +236,29 @@ impl Stackbar {
match STYLE.load() {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
// TODO: error handling
let _ = RoundRect(
hdc,
rect.left,
rect.top,
rect.right,
rect.bottom,
20,
20,
);
} else {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
// TODO: error handling
let _ = Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
BorderStyle::Rounded => {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
// TODO: error handling
let _ =
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
}
BorderStyle::Square => {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
// TODO: error handling
let _ = Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
@@ -267,9 +284,12 @@ impl Stackbar {
}
ReleaseDC(self.hwnd(), hdc);
DeleteObject(hpen);
DeleteObject(hbrush);
DeleteObject(hfont);
// TODO: error handling
let _ = DeleteObject(hpen);
// TODO: error handling
let _ = DeleteObject(hbrush);
// TODO: error handling
let _ = DeleteObject(hfont);
}
Ok(())
@@ -292,7 +312,7 @@ impl Stackbar {
match msg {
WM_LBUTTONDOWN => {
let stackbars_containers = STACKBARS_CONTAINERS.lock();
if let Some(container) = stackbars_containers.get(&hwnd.0) {
if let Some(container) = stackbars_containers.get(&(hwnd.0 as isize)) {
let x = l_param.0 as i32 & 0xFFFF;
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
@@ -302,11 +322,7 @@ impl Stackbar {
let focused_window_idx = container.focused_window_idx();
let focused_window_rect = WindowsApi::window_rect(
container
.focused_window()
.cloned()
.unwrap_or_default()
.hwnd(),
container.focused_window().cloned().unwrap_or_default().hwnd,
)
.unwrap_or_default();

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,13 @@ use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU8;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
use crate::should_act;
use crate::Window;
use crate::WindowManager;
use crate::WindowsApi;
use crate::REGEX_IDENTIFIERS;
use crate::TRANSPARENCY_BLACKLIST;
pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false);
pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200);
@@ -104,9 +106,18 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Monocle container is never transparent
if let Some(monocle) = ws.monocle_container() {
if let Some(window) = monocle.focused_window() {
if let Err(error) = window.opaque() {
if monitor_idx == focused_monitor_idx {
if let Err(error) = window.opaque() {
let hwnd = window.hwnd;
tracing::error!(
"failed to make monocle window {hwnd} opaque: {error}"
)
}
} else if let Err(error) = window.transparent() {
let hwnd = window.hwnd;
tracing::error!("failed to make monocle window {hwnd} opaque: {error}")
tracing::error!(
"failed to make monocle window {hwnd} transparent: {error}"
)
}
}
@@ -114,7 +125,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
let is_maximized = WindowsApi::is_zoomed(HWND(foreground_hwnd));
let is_maximized = WindowsApi::is_zoomed(foreground_hwnd);
if is_maximized {
if let Err(error) = Window::from(foreground_hwnd).opaque() {
@@ -125,6 +136,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
continue 'monitors;
}
let transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
for (idx, c) in ws.containers().iter().enumerate() {
// Update the transparency for all containers on this workspace
@@ -135,15 +149,37 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let focused_window_idx = c.focused_window_idx();
for (window_idx, window) in c.windows().iter().enumerate() {
if window_idx == focused_window_idx {
match window.transparent() {
Err(error) => {
let hwnd = foreground_hwnd;
tracing::error!(
"failed to make unfocused window {hwnd} transparent: {error}"
)
let mut should_make_transparent = true;
if !transparency_blacklist.is_empty() {
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (
window.title(),
window.exe(),
window.class(),
window.path(),
) {
let is_blacklisted = should_act(
&title,
&exe_name,
&class,
&path,
&transparency_blacklist,
&regex_identifiers,
)
.is_some();
should_make_transparent = !is_blacklisted;
}
Ok(..) => {
known_hwnds.lock().push(window.hwnd);
}
if should_make_transparent {
match window.transparent() {
Err(error) => {
let hwnd = foreground_hwnd;
tracing::error!("failed to make unfocused window {hwnd} transparent: {error}" )
}
Ok(..) => {
known_hwnds.lock().push(window.hwnd);
}
}
}
} else {

View File

@@ -2,10 +2,12 @@ use crate::border_manager;
use crate::com::SetCloak;
use crate::focus_manager;
use crate::stackbar_manager;
use crate::windows_api;
use crate::ANIMATIONS_IN_PROGRESS;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_TEMPORARY_DISABLED;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Display;
@@ -15,12 +17,12 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::time::Duration;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use color_eyre::eyre;
use color_eyre::Result;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use regex::Regex;
use schemars::JsonSchema;
use serde::ser::SerializeStruct;
@@ -29,9 +31,9 @@ use serde::Serialize;
use serde::Serializer;
use windows::Win32::Foundation::HWND;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
use crate::core::ApplicationIdentifier;
use crate::core::HidingBehaviour;
use crate::core::Rect;
use crate::animation::Animation;
use crate::styles::ExtendedWindowStyle;
@@ -39,9 +41,9 @@ use crate::styles::WindowStyle;
use crate::transparency_manager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::NO_TITLEBAR;
@@ -55,6 +57,7 @@ pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
pub struct Window {
pub hwnd: isize,
#[serde(skip)]
animation: Animation,
}
@@ -70,8 +73,8 @@ impl From<isize> for Window {
impl From<HWND> for Window {
fn from(value: HWND) -> Self {
Self {
hwnd: value.0,
animation: Animation::new(value.0),
hwnd: value.0 as isize,
animation: Animation::new(value.0 as isize),
}
}
}
@@ -145,7 +148,7 @@ impl Serialize for Window {
)?;
state.serialize_field(
"rect",
&WindowsApi::window_rect(self.hwnd()).unwrap_or_default(),
&WindowsApi::window_rect(self.hwnd).unwrap_or_default(),
)?;
state.end()
}
@@ -153,7 +156,32 @@ impl Serialize for Window {
impl Window {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn move_to_area(&mut self, current_area: &Rect, target_area: &Rect) -> Result<()> {
let current_rect = WindowsApi::window_rect(self.hwnd)?;
let x_diff = target_area.left - current_area.left;
let y_diff = target_area.top - current_area.top;
let x_ratio = f32::abs((target_area.right as f32) / (current_area.right as f32));
let y_ratio = f32::abs((target_area.bottom as f32) / (current_area.bottom as f32));
let window_relative_x = current_rect.left - current_area.left;
let window_relative_y = current_rect.top - current_area.top;
let corrected_relative_x = (window_relative_x as f32 * x_ratio) as i32;
let corrected_relative_y = (window_relative_y as f32 * y_ratio) as i32;
let window_x = current_area.left + corrected_relative_x;
let window_y = current_area.top + corrected_relative_y;
let new_rect = Rect {
left: x_diff + window_x,
top: y_diff + window_y,
right: current_rect.right,
bottom: current_rect.bottom,
};
//TODO: We might need to take into account the differences in DPI for the new_rect, unless
//we can use the xy ratios above to the right/bottom (width/height of window) as well?
self.set_position(&new_rect, true)
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
@@ -171,28 +199,28 @@ impl Window {
)
}
pub fn animate_position(&self, layout: &Rect, top: bool) -> Result<()> {
let hwnd = self.hwnd();
let curr_rect = WindowsApi::window_rect(hwnd).unwrap();
let target_rect = *layout;
pub fn animate_position(&self, start_rect: &Rect, target_rect: &Rect, top: bool) -> Result<()> {
let start_rect = *start_rect;
let target_rect = *target_rect;
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
let mut animation = self.animation;
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
border_manager::send_notification();
border_manager::send_notification(Some(self.hwnd));
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
let hwnd = self.hwnd;
std::thread::spawn(move || {
animation.animate(duration, |progress: f64| {
let new_rect = Animation::lerp_rect(&curr_rect, &target_rect, progress);
let new_rect = Animation::lerp_rect(&start_rect, &target_rect, progress);
if progress == 1.0 {
WindowsApi::position_window(hwnd, &new_rect, top)?;
if WindowsApi::foreground_window().unwrap_or_default() == hwnd.0 {
focus_manager::send_notification(hwnd.0)
if WindowsApi::foreground_window().unwrap_or_default() == hwnd {
focus_manager::send_notification(hwnd)
}
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
@@ -200,7 +228,7 @@ impl Window {
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
.store(false, Ordering::SeqCst);
border_manager::send_notification();
border_manager::send_notification(Some(hwnd));
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
@@ -208,7 +236,6 @@ impl Window {
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(hwnd, &new_rect, false)?;
// WindowsApi::position_window(hwnd, &new_rect, top)?;
WindowsApi::invalidate_rect(hwnd, None, false);
}
@@ -220,29 +247,29 @@ impl Window {
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
if WindowsApi::window_rect(self.hwnd())?.eq(layout) {
let window_rect = WindowsApi::window_rect(self.hwnd)?;
if window_rect.eq(layout) {
return Ok(());
}
if ANIMATION_ENABLED.load(Ordering::SeqCst)
&& !ANIMATION_TEMPORARY_DISABLED.load(Ordering::SeqCst)
{
self.animate_position(layout, top)
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
self.animate_position(&window_rect, layout, top)
} else {
WindowsApi::position_window(self.hwnd(), layout, top)
WindowsApi::position_window(self.hwnd, layout, top)
}
}
pub fn is_maximized(self) -> bool {
WindowsApi::is_zoomed(self.hwnd())
WindowsApi::is_zoomed(self.hwnd)
}
pub fn is_miminized(self) -> bool {
WindowsApi::is_iconic(self.hwnd())
WindowsApi::is_iconic(self.hwnd)
}
pub fn is_visible(self) -> bool {
WindowsApi::is_window_visible(self.hwnd())
WindowsApi::is_window_visible(self.hwnd)
}
pub fn hide(self) {
@@ -253,8 +280,8 @@ impl Window {
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
match *hiding_behaviour {
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd()),
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd()),
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd),
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd),
HidingBehaviour::Cloak => SetCloak(self.hwnd(), 1, 2),
}
}
@@ -271,18 +298,21 @@ impl Window {
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
match *hiding_behaviour {
HidingBehaviour::Hide | HidingBehaviour::Minimize => {
WindowsApi::restore_window(self.hwnd());
WindowsApi::restore_window(self.hwnd);
}
HidingBehaviour::Cloak => SetCloak(self.hwnd(), 1, 0),
}
}
pub fn minimize(self) {
WindowsApi::minimize_window(self.hwnd());
let exe = self.exe().unwrap_or_default();
if !exe.contains("komorebi-bar") {
WindowsApi::minimize_window(self.hwnd);
}
}
pub fn close(self) -> Result<()> {
WindowsApi::close_window(self.hwnd())
WindowsApi::close_window(self.hwnd)
}
pub fn maximize(self) {
@@ -294,7 +324,7 @@ impl Window {
programmatically_hidden_hwnds.remove(idx);
}
WindowsApi::maximize_window(self.hwnd());
WindowsApi::maximize_window(self.hwnd);
}
pub fn unmaximize(self) {
@@ -306,27 +336,27 @@ impl Window {
programmatically_hidden_hwnds.remove(idx);
}
WindowsApi::unmaximize_window(self.hwnd());
WindowsApi::unmaximize_window(self.hwnd);
}
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
// If the target window is already focused, do nothing.
if let Ok(ihwnd) = WindowsApi::foreground_window() {
if HWND(ihwnd) == self.hwnd() {
if ihwnd == self.hwnd {
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd)?)?;
}
return Ok(());
}
}
WindowsApi::raise_and_focus_window(self.hwnd())?;
WindowsApi::raise_and_focus_window(self.hwnd)?;
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd)?)?;
}
Ok(())
@@ -337,7 +367,7 @@ impl Window {
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd(),
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
}
@@ -356,31 +386,42 @@ impl Window {
WindowsApi::set_window_accent(self.hwnd, None)
}
#[allow(dead_code)]
#[cfg(target_pointer_width = "64")]
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
WindowsApi::update_style(self.hwnd, isize::try_from(style.bits())?)
}
#[cfg(target_pointer_width = "32")]
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
WindowsApi::update_style(self.hwnd, i32::try_from(style.bits())?)
}
#[cfg(target_pointer_width = "64")]
pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> Result<()> {
WindowsApi::update_ex_style(self.hwnd(), isize::try_from(style.bits())?)
WindowsApi::update_ex_style(self.hwnd, isize::try_from(style.bits())?)
}
#[cfg(target_pointer_width = "32")]
pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> Result<()> {
WindowsApi::update_ex_style(self.hwnd, i32::try_from(style.bits())?)
}
pub fn style(self) -> Result<WindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd)?)?;
Ok(WindowStyle::from_bits_truncate(bits))
}
pub fn ex_style(self) -> Result<ExtendedWindowStyle> {
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd)?)?;
Ok(ExtendedWindowStyle::from_bits_truncate(bits))
}
pub fn title(self) -> Result<String> {
WindowsApi::window_text_w(self.hwnd())
WindowsApi::window_text_w(self.hwnd)
}
pub fn path(self) -> Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
let handle = WindowsApi::process_handle(process_id)?;
let path = WindowsApi::exe_path(handle);
WindowsApi::close_process(handle)?;
@@ -388,23 +429,28 @@ impl Window {
}
pub fn exe(self) -> Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
let handle = WindowsApi::process_handle(process_id)?;
let exe = WindowsApi::exe(handle);
WindowsApi::close_process(handle)?;
exe
}
pub fn process_id(self) -> u32 {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
process_id
}
pub fn class(self) -> Result<String> {
WindowsApi::real_window_class_w(self.hwnd())
WindowsApi::real_window_class_w(self.hwnd)
}
pub fn is_cloaked(self) -> Result<bool> {
WindowsApi::is_window_cloaked(self.hwnd())
WindowsApi::is_window_cloaked(self.hwnd)
}
pub fn is_window(self) -> bool {
WindowsApi::is_window(self.hwnd())
WindowsApi::is_window(self.hwnd)
}
pub fn remove_title_bar(self) -> Result<()> {
@@ -433,7 +479,7 @@ impl Window {
debug.is_window = true;
let rect = WindowsApi::window_rect(self.hwnd()).unwrap_or_default();
let rect = WindowsApi::window_rect(self.hwnd).unwrap_or_default();
if rect.right < MINIMUM_WIDTH.load(Ordering::SeqCst) {
return Ok(false);
@@ -516,7 +562,7 @@ pub struct RuleDebug {
pub class: Option<String>,
pub path: Option<String>,
pub matches_permaignore_class: Option<String>,
pub matches_float_identifier: Option<MatchingRule>,
pub matches_ignore_identifier: Option<MatchingRule>,
pub matches_managed_override: Option<MatchingRule>,
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
@@ -545,16 +591,16 @@ fn window_is_eligible(
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let float_identifiers = FLOAT_IDENTIFIERS.lock();
let ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let should_float = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&float_identifiers,
&ignore_identifiers,
&regex_identifiers,
) {
debug.matches_float_identifier = Some(rule);
debug.matches_ignore_identifier = Some(rule);
true
} else {
false
@@ -618,8 +664,23 @@ fn window_is_eligible(
titlebars_removed.contains(exe_name)
};
if exe_name.contains("firefox") {
std::thread::sleep(Duration::from_millis(10));
{
let slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
let should_sleep = should_act(
title,
exe_name,
class,
path,
&slow_application_identifiers,
&regex_identifiers,
)
.is_some();
if should_sleep {
std::thread::sleep(Duration::from_millis(
SLOW_APPLICATION_COMPENSATION_TIME.load(Ordering::SeqCst),
));
}
}
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))

View File

@@ -16,37 +16,41 @@ use hotwatch::notify::ErrorKind as NotifyErrorKind;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixListener;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::custom_layout::CustomLayout;
use komorebi_core::Arrangement;
use komorebi_core::Axis;
use komorebi_core::BorderStyle;
use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
use komorebi_core::Layout;
use komorebi_core::MoveBehaviour;
use komorebi_core::OperationBehaviour;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::StackbarLabel;
use komorebi_core::WindowContainerBehaviour;
use crate::core::config_generation::MatchingRule;
use crate::core::custom_layout::CustomLayout;
use crate::core::Arrangement;
use crate::core::Axis;
use crate::core::BorderImplementation;
use crate::core::BorderStyle;
use crate::core::CycleDirection;
use crate::core::DefaultLayout;
use crate::core::FocusFollowsMouseImplementation;
use crate::core::HidingBehaviour;
use crate::core::Layout;
use crate::core::MoveBehaviour;
use crate::core::OperationBehaviour;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::core::Sizing;
use crate::core::StackbarLabel;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use crate::border_manager;
use crate::border_manager::STYLE;
use crate::config_generation::WorkspaceMatchingRule;
use crate::container::Container;
use crate::core::StackbarMode;
use crate::current_virtual_desktop;
use crate::load_configuration;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::should_act_individual;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_MODE;
@@ -63,24 +67,23 @@ use crate::winevent_listener;
use crate::workspace::Workspace;
use crate::BorderColours;
use crate::Colour;
use crate::CrossBoundaryBehaviour;
use crate::Rgb;
use crate::WorkspaceRule;
use crate::ANIMATION_TEMPORARY_DISABLED;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::HOME_DIR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::REMOVE_TITLEBARS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use komorebi_core::StackbarMode;
use crate::WORKSPACE_MATCHING_RULES;
#[derive(Debug)]
pub struct WindowManager {
@@ -90,8 +93,9 @@ pub struct WindowManager {
pub is_paused: bool,
pub work_area_offset: Option<Rect>,
pub resize_delta: i32,
pub window_container_behaviour: WindowContainerBehaviour,
pub window_management_behaviour: WindowManagementBehaviour,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub cross_boundary_behaviour: CrossBoundaryBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub mouse_follows_focus: bool,
@@ -109,6 +113,7 @@ pub struct State {
pub is_paused: bool,
pub resize_delta: i32,
pub new_window_behaviour: WindowContainerBehaviour,
pub float_override: bool,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub work_area_offset: Option<Rect>,
@@ -117,6 +122,54 @@ pub struct State {
pub has_pending_raise_op: bool,
}
impl State {
pub fn has_been_modified(&self, wm: &WindowManager) -> bool {
let new = Self::from(wm);
if self.monitors != new.monitors {
return true;
}
if self.is_paused != new.is_paused {
return true;
}
if self.new_window_behaviour != new.new_window_behaviour {
return true;
}
if self.float_override != new.float_override {
return true;
}
if self.cross_monitor_move_behaviour != new.cross_monitor_move_behaviour {
return true;
}
if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour {
return true;
}
if self.work_area_offset != new.work_area_offset {
return true;
}
if self.focus_follows_mouse != new.focus_follows_mouse {
return true;
}
if self.mouse_follows_focus != new.mouse_follows_focus {
return true;
}
if self.has_pending_raise_op != new.has_pending_raise_op {
return true;
}
false
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GlobalState {
@@ -133,14 +186,15 @@ pub struct GlobalState {
pub stackbar_tab_width: i32,
pub stackbar_height: i32,
pub remove_titlebars: bool,
pub float_identifiers: Vec<MatchingRule>,
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Vec<MatchingRule>,
pub manage_identifiers: Vec<MatchingRule>,
pub layered_whitelist: Vec<MatchingRule>,
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
pub monitor_index_preferences: HashMap<usize, Rect>,
pub display_index_preferences: HashMap<usize, String>,
pub workspace_rules: HashMap<String, WorkspaceRule>,
pub workspace_rules: Vec<WorkspaceMatchingRule>,
pub window_hiding_behaviour: HidingBehaviour,
pub configuration_dir: PathBuf,
pub data_dir: PathBuf,
@@ -161,6 +215,9 @@ impl Default for GlobalState {
monocle: Option::from(Colour::Rgb(Rgb::from(
border_manager::MONOCLE.load(Ordering::SeqCst),
))),
floating: Option::from(Colour::Rgb(Rgb::from(
border_manager::FLOATING.load(Ordering::SeqCst),
))),
unfocused: Option::from(Colour::Rgb(Rgb::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
))),
@@ -182,14 +239,14 @@ impl Default for GlobalState {
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
display_index_preferences: DISPLAY_INDEX_PREFERENCES.lock().clone(),
workspace_rules: WORKSPACE_RULES.lock().clone(),
workspace_rules: WORKSPACE_MATCHING_RULES.lock().clone(),
window_hiding_behaviour: *HIDING_BEHAVIOUR.lock(),
configuration_dir: HOME_DIR.clone(),
data_dir: DATA_DIR.clone(),
@@ -211,7 +268,8 @@ impl From<&WindowManager> for State {
is_paused: wm.is_paused,
work_area_offset: wm.work_area_offset,
resize_delta: wm.resize_delta,
new_window_behaviour: wm.window_container_behaviour,
new_window_behaviour: wm.window_management_behaviour.current_behaviour,
float_override: wm.window_management_behaviour.float_override,
cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour,
focus_follows_mouse: wm.focus_follows_mouse,
mouse_follows_focus: wm.mouse_follows_focus,
@@ -231,7 +289,6 @@ struct EnforceWorkspaceRuleOp {
target_monitor_idx: usize,
target_workspace_idx: usize,
}
impl EnforceWorkspaceRuleOp {
const fn is_origin(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
self.origin_monitor_idx == monitor_idx && self.origin_workspace_idx == workspace_idx
@@ -272,8 +329,9 @@ impl WindowManager {
is_paused: false,
virtual_desktop_id: current_virtual_desktop(),
work_area_offset: None,
window_container_behaviour: WindowContainerBehaviour::Create,
window_management_behaviour: WindowManagementBehaviour::default(),
cross_monitor_move_behaviour: MoveBehaviour::Swap,
cross_boundary_behaviour: CrossBoundaryBehaviour::Workspace,
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
resize_delta: 50,
focus_follows_mouse: None,
@@ -304,22 +362,52 @@ impl WindowManager {
StaticConfig::reload(pathbuf, self)
}
pub fn window_container_behaviour(
pub fn window_management_behaviour(
&self,
monitor_idx: usize,
workspace_idx: usize,
) -> WindowContainerBehaviour {
) -> WindowManagementBehaviour {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.workspaces().get(workspace_idx) {
return if workspace.containers().is_empty() {
WindowContainerBehaviour::Create
let current_behaviour =
if let Some(behaviour) = workspace.window_container_behaviour() {
if workspace.containers().is_empty()
&& matches!(behaviour, WindowContainerBehaviour::Append)
{
// You can't append to an empty workspace
WindowContainerBehaviour::Create
} else {
*behaviour
}
} else if workspace.containers().is_empty()
&& matches!(
self.window_management_behaviour.current_behaviour,
WindowContainerBehaviour::Append
)
{
// You can't append to an empty workspace
WindowContainerBehaviour::Create
} else {
self.window_management_behaviour.current_behaviour
};
let float_override = if let Some(float_override) = workspace.float_override() {
*float_override
} else {
self.window_container_behaviour
self.window_management_behaviour.float_override
};
return WindowManagementBehaviour {
current_behaviour,
float_override,
};
}
}
WindowContainerBehaviour::Create
WindowManagementBehaviour {
current_behaviour: WindowContainerBehaviour::Create,
float_override: self.window_management_behaviour.float_override,
}
}
#[tracing::instrument(skip(self))]
@@ -447,7 +535,8 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.focused_workspace_idx();
let workspace_rules = WORKSPACE_RULES.lock();
let workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
// Go through all the monitors and workspaces
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
@@ -457,63 +546,61 @@ impl WindowManager {
let exe_name = window.exe()?;
let title = window.title()?;
let class = window.class()?;
let path = window.path()?;
let mut found_workspace_rule = workspace_rules.get(&exe_name);
if found_workspace_rule.is_none() {
found_workspace_rule = workspace_rules.get(&title);
}
if found_workspace_rule.is_none() {
found_workspace_rule = workspace_rules.get(&class);
}
if found_workspace_rule.is_none() {
for (k, v) in workspace_rules.iter() {
if let Ok(re) = Regex::new(k) {
if re.is_match(&exe_name) {
found_workspace_rule = Some(v);
for rule in &*workspace_matching_rules {
let matched = match &rule.matching_rule {
MatchingRule::Simple(r) => should_act_individual(
&title,
&exe_name,
&class,
&path,
r,
&regex_identifiers,
),
MatchingRule::Composite(r) => {
let mut composite_results = vec![];
for identifier in r {
composite_results.push(should_act_individual(
&title,
&exe_name,
&class,
&path,
identifier,
&regex_identifiers,
));
}
if re.is_match(&title) {
found_workspace_rule = Some(v);
}
if re.is_match(&class) {
found_workspace_rule = Some(v);
}
composite_results.iter().all(|&x| x)
}
}
}
};
// If the executable names or titles of any of those windows are in our rules map
if let Some((monitor_idx, workspace_idx, apply_on_first_show_only)) =
found_workspace_rule
{
if *apply_on_first_show_only {
if !already_moved_window_handles.contains(&window.hwnd) {
already_moved_window_handles.insert(window.hwnd);
if matched {
if rule.initial_only {
if !already_moved_window_handles.contains(&window.hwnd) {
already_moved_window_handles.insert(window.hwnd);
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
&mut to_move,
);
}
} else {
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
*monitor_idx,
*workspace_idx,
rule.monitor_index,
rule.workspace_index,
&mut to_move,
);
}
} else {
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
*monitor_idx,
*workspace_idx,
&mut to_move,
);
}
}
}
@@ -816,35 +903,46 @@ impl WindowManager {
let rect = self.focused_monitor_size()?;
WindowsApi::center_cursor_in_rect(&rect)?;
match WindowsApi::raise_and_focus_window(desktop_window.hwnd()) {
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
Ok(()) => {}
Err(error) => {
tracing::warn!("{} {}:{}", error, file!(), line!());
}
}
}
}
} else {
if self.focused_workspace()?.is_empty() {
let desktop_window = Window::from(WindowsApi::desktop_window()?);
// if we passed false for follow_focus and there is a container on the workspace
if !follow_focus && self.focused_container_mut().is_ok() {
// and we have a stack with >1 windows
if self.focused_container_mut()?.windows().len() > 1
// and we don't have a maxed window
&& self.focused_workspace()?.maximized_window().is_none()
// and we don't have a monocle container
&& self.focused_workspace()?.monocle_container().is_none()
{
if let Ok(window) = self.focused_window_mut() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
Ok(()) => {}
Err(error) => {
tracing::warn!("{} {}:{}", error, file!(), line!());
}
}
}
}
// This is to correctly restore and focus when switching to a workspace which
// contains a managed maximized window
if !follow_focus {
// if we passed false for follow_focus and there is a container on the workspace
if self.focused_container_mut().is_ok() {
// and we have a stack with >1 windows
if self.focused_container_mut()?.windows().len() > 1
// and we don't have a maxed window
&& self.focused_workspace()?.maximized_window().is_none()
// and we don't have a monocle container
&& self.focused_workspace()?.monocle_container().is_none()
// and we don't have any floating windows that should show on top
&& self.focused_workspace()?.floating_windows().is_empty()
{
if let Ok(window) = self.focused_window_mut() {
if trigger_focus {
window.focus(self.mouse_follows_focus)?;
}
}
}
}
// This is to correctly restore and focus when switching to a workspace which
// contains a managed maximized window
if let Some(window) = self.focused_workspace()?.maximized_window() {
window.restore();
if trigger_focus {
@@ -953,9 +1051,18 @@ impl WindowManager {
let no_titlebar = NO_TITLEBAR.lock();
let known_transparent_hwnds = transparency_manager::known_hwnds();
let border_implementation = border_manager::IMPLEMENTATION.load();
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
if let Some(monocle) = workspace.monocle_container() {
for window in monocle.windows() {
if matches!(border_implementation, BorderImplementation::Windows) {
window.remove_accent()?;
}
}
}
for containers in workspace.containers_mut() {
for window in containers.windows_mut() {
if no_titlebar.contains(&window.exe()?) {
@@ -966,7 +1073,9 @@ impl WindowManager {
window.opaque()?;
}
window.remove_accent()?;
if matches!(border_implementation, BorderImplementation::Windows) {
window.remove_accent()?;
}
window.restore();
}
@@ -1109,7 +1218,6 @@ impl WindowManager {
follow: bool,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
ANIMATION_TEMPORARY_DISABLED.store(true, Ordering::SeqCst);
tracing::info!("moving container");
@@ -1117,7 +1225,7 @@ impl WindowManager {
if focused_monitor_idx == monitor_idx {
if let Some(workspace_idx) = workspace_idx {
return self.move_container_to_workspace(workspace_idx, follow);
return self.move_container_to_workspace(workspace_idx, follow, None);
}
}
@@ -1128,6 +1236,8 @@ impl WindowManager {
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let current_area = *monitor.work_area_size();
let workspace = monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
@@ -1136,16 +1246,23 @@ impl WindowManager {
bail!("cannot move native maximized window to another monitor or workspace");
}
let container = workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
let container_hwnds = container
.windows()
let foreground_hwnd = WindowsApi::foreground_window()?;
let floating_window_index = workspace
.floating_windows()
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
.position(|w| w.hwnd == foreground_hwnd);
let floating_window =
floating_window_index.map(|idx| workspace.floating_windows_mut().remove(idx));
let container = if floating_window_index.is_none() {
Some(
workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?,
)
} else {
None
};
monitor.update_focused_workspace(offset)?;
let target_monitor = self
@@ -1153,18 +1270,36 @@ impl WindowManager {
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.add_container(container, workspace_idx)?;
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
}
let target_workspace = target_monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no focused workspace on target monitor"))?;
if let Some(workspace) = target_monitor.focused_workspace() {
if !*workspace.tile() {
for hwnd in container_hwnds {
Window::from(hwnd).center(target_monitor.work_area_size())?;
if let Some(window) = floating_window {
target_workspace.floating_windows_mut().push(window);
Window::from(window.hwnd)
.move_to_area(&current_area, target_monitor.work_area_size())?;
} else if let Some(container) = container {
let container_hwnds = container
.windows()
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
target_monitor.add_container(container, workspace_idx)?;
if let Some(workspace) = target_monitor.focused_workspace() {
if !*workspace.tile() {
for hwnd in container_hwnds {
Window::from(hwnd)
.move_to_area(&current_area, target_monitor.work_area_size())?;
}
}
}
} else {
bail!("failed to find a window to move");
}
target_monitor.load_focused_workspace(mouse_follows_focus)?;
@@ -1181,15 +1316,17 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)?;
ANIMATION_TEMPORARY_DISABLED.store(false, Ordering::SeqCst);
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
pub fn move_container_to_workspace(
&mut self,
idx: usize,
follow: bool,
direction: Option<OperationDirection>,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
ANIMATION_TEMPORARY_DISABLED.store(true, Ordering::SeqCst);
tracing::info!("moving container");
@@ -1198,13 +1335,11 @@ impl WindowManager {
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
monitor.move_container_to_workspace(idx, follow)?;
monitor.move_container_to_workspace(idx, follow, direction)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
self.update_focused_workspace(mouse_follows_focus, true)?;
ANIMATION_TEMPORARY_DISABLED.store(false, Ordering::SeqCst);
Ok(())
}
@@ -1242,6 +1377,7 @@ impl WindowManager {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace()?;
let workspace_idx = self.focused_workspace_idx()?;
tracing::info!("focusing container");
@@ -1253,6 +1389,70 @@ impl WindowManager {
let mut cross_monitor_monocle = false;
// this is for when we are scrolling across workspaces like PaperWM
if new_idx.is_none()
&& matches!(
self.cross_boundary_behaviour,
CrossBoundaryBehaviour::Workspace
)
&& matches!(
direction,
OperationDirection::Left | OperationDirection::Right
)
{
let workspace_count = if let Some(monitor) = self.focused_monitor() {
monitor.workspaces().len()
} else {
1
};
let next_idx = match direction {
OperationDirection::Left => match workspace_idx {
0 => workspace_count - 1,
n => n - 1,
},
OperationDirection::Right => match workspace_idx {
n if n == workspace_count - 1 => 0,
n => n + 1,
},
_ => workspace_idx,
};
self.focus_workspace(next_idx)?;
if let Ok(focused_workspace) = self.focused_workspace_mut() {
if focused_workspace.monocle_container().is_none() {
match direction {
OperationDirection::Left => match focused_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.rightmost_index(focused_workspace.containers().len());
focused_workspace.focus_container(target_index);
}
Layout::Custom(_) => {
focused_workspace.focus_container(
focused_workspace.containers().len().saturating_sub(1),
);
}
},
OperationDirection::Right => match focused_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(focused_workspace.containers().len());
focused_workspace.focus_container(target_index);
}
Layout::Custom(_) => {
focused_workspace.focus_container(0);
}
},
_ => {}
};
}
}
return Ok(());
}
// if there is no container in that direction for this workspace
match new_idx {
None => {
@@ -1261,17 +1461,44 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
self.focus_monitor(monitor_idx)?;
let mouse_follows_focus = self.mouse_follows_focus;
if let Ok(focused_workspace) = self.focused_workspace() {
if let Ok(focused_workspace) = self.focused_workspace_mut() {
if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(self.mouse_follows_focus)?;
window.focus(mouse_follows_focus)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
window.hwnd(),
window.hwnd,
)?)?;
cross_monitor_monocle = true;
}
} else {
match direction {
OperationDirection::Left => match focused_workspace.layout() {
Layout::Default(layout) => {
let target_index = layout
.rightmost_index(focused_workspace.containers().len());
focused_workspace.focus_container(target_index);
}
Layout::Custom(_) => {
focused_workspace.focus_container(
focused_workspace.containers().len().saturating_sub(1),
);
}
},
OperationDirection::Right => match focused_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(focused_workspace.containers().len());
focused_workspace.focus_container(target_index);
}
Layout::Custom(_) => {
focused_workspace.focus_container(0);
}
},
_ => {}
};
}
}
}
@@ -1295,6 +1522,7 @@ impl WindowManager {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace()?;
let workspace_idx = self.focused_workspace_idx()?;
// removing this messes up the monitor / container / window index somewhere
// and results in the wrong window getting moved across the monitor boundary
@@ -1308,12 +1536,42 @@ impl WindowManager {
let origin_monitor_idx = self.focused_monitor_idx();
let target_container_idx = workspace.new_idx_for_direction(direction);
let animation_temporarily_disabled = if target_container_idx.is_none() {
ANIMATION_TEMPORARY_DISABLED.store(true, Ordering::SeqCst);
true
} else {
false
};
// this is for when we are scrolling across workspaces like PaperWM
if target_container_idx.is_none()
&& matches!(
self.cross_boundary_behaviour,
CrossBoundaryBehaviour::Workspace
)
&& matches!(
direction,
OperationDirection::Left | OperationDirection::Right
)
{
let workspace_count = if let Some(monitor) = self.focused_monitor() {
monitor.workspaces().len()
} else {
1
};
let next_idx = match direction {
OperationDirection::Left => match workspace_idx {
0 => workspace_count - 1,
n => n - 1,
},
OperationDirection::Right => match workspace_idx {
n if n == workspace_count - 1 => 0,
n => n + 1,
},
_ => workspace_idx,
};
// passing the direction here is how we handle whether to insert at the front
// or the back of the container vecdeque in the target workspace
self.move_container_to_workspace(next_idx, true, Some(direction))?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
return Ok(());
}
match target_container_idx {
// If there is nowhere to move on the current workspace, try to move it onto the monitor
@@ -1357,12 +1615,78 @@ impl WindowManager {
// get a mutable ref to the focused workspace on the target monitor
let target_workspace = self.focused_workspace_mut()?;
// insert the origin container into the focused workspace on the target monitor
// at the position where the currently focused container on that workspace is
target_workspace.insert_container_at_idx(
target_workspace.focused_container_idx(),
origin_container,
);
match direction {
OperationDirection::Left => {
// insert the origin container into the focused workspace on the target monitor
// at the back (or rightmost position) if we are moving across a boundary to
// the left (back = right side of the target)
match target_workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
target_workspace.add_container_to_front(origin_container);
}
DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace
.insert_container_at_idx(0, origin_container);
} else {
target_workspace
.add_container_to_back(origin_container);
}
}
_ => {
target_workspace.add_container_to_back(origin_container);
}
},
Layout::Custom(_) => {
target_workspace.add_container_to_back(origin_container);
}
}
}
OperationDirection::Right => {
// insert the origin container into the focused workspace on the target monitor
// at the front (or leftmost position) if we are moving across a boundary to the
// right (front = left side of the target)
match target_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(target_workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace
.add_container_to_back(origin_container);
} else {
target_workspace.insert_container_at_idx(
target_index,
origin_container,
);
}
}
_ => {
target_workspace.insert_container_at_idx(
target_index,
origin_container,
);
}
}
}
Layout::Custom(_) => {
target_workspace.add_container_to_front(origin_container);
}
}
}
OperationDirection::Up | OperationDirection::Down => {
// insert the origin container into the focused workspace on the target monitor
// at the position where the currently focused container on that workspace is
target_workspace.insert_container_at_idx(
target_workspace.focused_container_idx(),
origin_container,
);
}
};
// if there is only one container on the target workspace after the insertion
// it means that there won't be one swapped back, so we have to decrement the
@@ -1441,10 +1765,6 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)?;
if animation_temporarily_disabled {
ANIMATION_TEMPORARY_DISABLED.store(false, Ordering::SeqCst);
}
Ok(())
}
@@ -1530,6 +1850,31 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn focus_container_window(&mut self, idx: usize) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("focusing container window at index {idx}");
let container = self.focused_container_mut()?;
let len = NonZeroUsize::new(container.windows().len())
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
if len.get() == 1 {
bail!("there is only one window in this container");
}
if container.windows().get(idx).is_none() {
bail!("there is no window in this container at index {idx}");
}
container.focus_window(idx);
container.load_focused_window();
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn stack_all(&mut self) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -2390,7 +2735,7 @@ impl WindowManager {
}
pub fn monitor_idx_from_window(&mut self, window: Window) -> Option<usize> {
let hmonitor = WindowsApi::monitor_from_window(window.hwnd());
let hmonitor = WindowsApi::monitor_from_window(window.hwnd);
for (i, monitor) in self.monitors().iter().enumerate() {
if monitor.id() == hmonitor {

View File

@@ -102,6 +102,48 @@ impl WindowManagerEvent {
}
}
pub const fn hwnd(self) -> isize {
self.window().hwnd
}
pub const fn title(self) -> &'static str {
match self {
WindowManagerEvent::Destroy(_, _) => "Destroy",
WindowManagerEvent::FocusChange(_, _) => "FocusChange",
WindowManagerEvent::Hide(_, _) => "Hide",
WindowManagerEvent::Cloak(_, _) => "Cloak",
WindowManagerEvent::Minimize(_, _) => "Minimize",
WindowManagerEvent::Show(_, _) => "Show",
WindowManagerEvent::Uncloak(_, _) => "Uncloak",
WindowManagerEvent::MoveResizeStart(_, _) => "MoveResizeStart",
WindowManagerEvent::MoveResizeEnd(_, _) => "MoveResizeEnd",
WindowManagerEvent::MouseCapture(_, _) => "MouseCapture",
WindowManagerEvent::Manage(_) => "Manage",
WindowManagerEvent::Unmanage(_) => "Unmanage",
WindowManagerEvent::Raise(_) => "Raise",
WindowManagerEvent::TitleUpdate(_, _) => "TitleUpdate",
}
}
pub fn winevent(self) -> Option<String> {
match self {
WindowManagerEvent::Destroy(event, _)
| WindowManagerEvent::FocusChange(event, _)
| WindowManagerEvent::Hide(event, _)
| WindowManagerEvent::Cloak(event, _)
| WindowManagerEvent::Minimize(event, _)
| WindowManagerEvent::Show(event, _)
| WindowManagerEvent::Uncloak(event, _)
| WindowManagerEvent::MoveResizeStart(event, _)
| WindowManagerEvent::MoveResizeEnd(event, _)
| WindowManagerEvent::MouseCapture(event, _)
| WindowManagerEvent::TitleUpdate(event, _) => Some(event.to_string()),
WindowManagerEvent::Manage(_)
| WindowManagerEvent::Unmanage(_)
| WindowManagerEvent::Raise(_) => None,
}
}
pub fn from_win_event(winevent: WinEvent, window: Window) -> Option<Self> {
match winevent {
WinEvent::ObjectDestroy => Option::from(Self::Destroy(winevent, window)),
@@ -151,7 +193,10 @@ impl WindowManagerEvent {
)
.is_some();
if should_trigger_show {
// should not trigger show on minimized windows, for example when firefox sends
// this message due to youtube autoplay changing the window title
// https://github.com/LGUG2Z/komorebi/issues/941
if should_trigger_show && !window.is_miminized() {
Option::from(Self::Show(winevent, window))
} else {
Option::from(Self::TitleUpdate(winevent, window))

View File

@@ -14,6 +14,7 @@ use windows::Win32::Foundation::CloseHandle;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Foundation::HINSTANCE;
use windows::Win32::Foundation::HMODULE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
@@ -134,7 +135,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use komorebi_core::Rect;
use crate::core::Rect;
use crate::container::Container;
use crate::monitor;
@@ -146,6 +147,14 @@ use crate::Window;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
macro_rules! as_ptr {
($value:expr) => {
$value as *mut core::ffi::c_void
};
}
pub(crate) use as_ptr;
pub enum WindowsResult<T, E> {
Err(E),
Ok(T),
@@ -187,10 +196,10 @@ macro_rules! impl_process_windows_crate_integer_wrapper_result {
$(
impl ProcessWindowsCrateResult<$deref> for $input {
fn process(self) -> Result<$deref> {
if self == $input(0) {
if self == $input(std::ptr::null_mut()) {
Err(std::io::Error::last_os_error().into())
} else {
Ok(self.0)
Ok(self.0 as $deref)
}
}
}
@@ -219,9 +228,16 @@ impl WindowsApi {
callback: MONITORENUMPROC,
callback_data_address: isize,
) -> Result<()> {
unsafe { EnumDisplayMonitors(HDC(0), None, callback, LPARAM(callback_data_address)) }
.ok()
.process()
unsafe {
EnumDisplayMonitors(
HDC(std::ptr::null_mut()),
None,
callback,
LPARAM(callback_data_address),
)
}
.ok()
.process()
}
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
@@ -239,11 +255,17 @@ impl WindowsApi {
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
'read: for display in win32_display_data::connected_displays_all().flatten() {
let path = display.device_path.clone();
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let (device, device_id) = if path.is_empty() {
(String::from("UNKNOWN"), String::from("UNKNOWN"))
} else {
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
(device, device_id)
};
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
@@ -282,7 +304,7 @@ impl WindowsApi {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
while *preference > monitors.elements().len() {
monitors.elements_mut().reserve(1);
monitors.elements_mut().push_back(Monitor::placeholder());
}
monitors.elements_mut().insert(*preference, m);
@@ -291,6 +313,10 @@ impl WindowsApi {
}
}
monitors
.elements_mut()
.retain(|m| m.name().ne("PLACEHOLDER"));
Ok(())
}
@@ -318,8 +344,8 @@ impl WindowsApi {
for container in workspace.containers_mut() {
for window in container.windows() {
if Self::monitor_name_from_window(window.hwnd())? != monitor_name {
windows_on_other_monitors.push(window.hwnd().0);
if Self::monitor_name_from_window(window.hwnd)? != monitor_name {
windows_on_other_monitors.push(window.hwnd);
}
}
}
@@ -337,32 +363,34 @@ impl WindowsApi {
unsafe { AllowSetForegroundWindow(process_id) }.process()
}
pub fn monitor_from_window(hwnd: HWND) -> isize {
pub fn monitor_from_window(hwnd: isize) -> 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
unsafe { MonitorFromWindow(HWND(as_ptr!(hwnd)), MONITOR_DEFAULTTONEAREST) }.0 as isize
}
pub fn monitor_name_from_window(hwnd: HWND) -> Result<String> {
pub fn monitor_name_from_window(hwnd: isize) -> Result<String> {
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
Ok(
Self::monitor(unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0)?
.name()
.to_string(),
)
Ok(Self::monitor(
unsafe { MonitorFromWindow(HWND(as_ptr!(hwnd)), MONITOR_DEFAULTTONEAREST) }.0 as isize,
)?
.name()
.to_string())
}
pub fn monitor_from_point(point: POINT) -> isize {
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0 as isize
}
/// position window resizes the target window to the given layout, adjusting
/// the layout to account for any window shadow borders (the window painted
/// region will match layout on completion).
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
pub fn position_window(hwnd: isize, layout: &Rect, top: bool) -> Result<()> {
let hwnd = HWND(as_ptr!(hwnd));
let mut flags = SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_COPY_BITS
@@ -398,22 +426,32 @@ impl WindowsApi {
Self::set_window_pos(hwnd, &rect, HWND_TOP, flags.bits())
}
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
unsafe { BringWindowToTop(hwnd) }.process()
pub fn bring_window_to_top(hwnd: isize) -> Result<()> {
unsafe { BringWindowToTop(HWND(as_ptr!(hwnd))) }.process()
}
// Raise the window to the top of the Z order, but do not activate or focus
// it. Use raise_and_focus_window to activate and focus a window.
pub fn raise_window(hwnd: HWND) -> Result<()> {
pub fn raise_window(hwnd: isize) -> Result<()> {
let flags = SetWindowPosition::NO_MOVE | SetWindowPosition::NO_ACTIVATE;
let position = HWND_TOP;
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
Self::set_window_pos(
HWND(as_ptr!(hwnd)),
&Rect::default(),
position,
flags.bits(),
)
}
pub fn set_border_pos(hwnd: HWND, layout: &Rect, position: HWND) -> Result<()> {
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
Self::set_window_pos(hwnd, layout, position, flags.bits())
Self::set_window_pos(
HWND(as_ptr!(hwnd)),
layout,
HWND(as_ptr!(position)),
flags.bits(),
)
}
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
@@ -432,7 +470,9 @@ impl WindowsApi {
.process()
}
pub fn move_window(hwnd: HWND, layout: &Rect, repaint: bool) -> Result<()> {
pub fn move_window(hwnd: isize, layout: &Rect, repaint: bool) -> Result<()> {
let hwnd = HWND(as_ptr!(hwnd));
let shadow_rect = Self::shadow_rect(hwnd).unwrap_or_default();
let rect = Rect {
left: layout.left + shadow_rect.left,
@@ -443,13 +483,16 @@ impl WindowsApi {
unsafe { MoveWindow(hwnd, rect.left, rect.top, rect.right, rect.bottom, repaint) }.process()
}
pub fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
pub fn show_window(hwnd: isize, command: SHOW_WINDOW_CMD) {
// BOOL is returned but does not signify whether or not the operation was succesful
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
unsafe { ShowWindow(hwnd, command) };
// TODO: error handling
unsafe {
let _ = ShowWindow(HWND(as_ptr!(hwnd)), command);
};
}
pub fn minimize_window(hwnd: HWND) {
pub fn minimize_window(hwnd: isize) {
Self::show_window(hwnd, SW_MINIMIZE);
}
@@ -457,26 +500,26 @@ impl WindowsApi {
unsafe { PostMessageW(hwnd, message, wparam, lparam) }.process()
}
pub fn close_window(hwnd: HWND) -> Result<()> {
match Self::post_message(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)) {
pub fn close_window(hwnd: isize) -> Result<()> {
match Self::post_message(HWND(as_ptr!(hwnd)), WM_CLOSE, WPARAM(0), LPARAM(0)) {
Ok(()) => Ok(()),
Err(_) => Err(anyhow!("could not close window")),
}
}
pub fn hide_window(hwnd: HWND) {
pub fn hide_window(hwnd: isize) {
Self::show_window(hwnd, SW_HIDE);
}
pub fn restore_window(hwnd: HWND) {
pub fn restore_window(hwnd: isize) {
Self::show_window(hwnd, SW_SHOWNOACTIVATE);
}
pub fn unmaximize_window(hwnd: HWND) {
pub fn unmaximize_window(hwnd: isize) {
Self::show_window(hwnd, SW_NORMAL);
}
pub fn maximize_window(hwnd: HWND) {
pub fn maximize_window(hwnd: isize) {
Self::show_window(hwnd, SW_MAXIMIZE);
}
@@ -484,7 +527,7 @@ impl WindowsApi {
unsafe { GetForegroundWindow() }.process()
}
pub fn raise_and_focus_window(hwnd: HWND) -> Result<()> {
pub fn raise_and_focus_window(hwnd: isize) -> Result<()> {
let event = [INPUT {
r#type: INPUT_MOUSE,
..Default::default()
@@ -496,7 +539,7 @@ impl WindowsApi {
SendInput(&event, size_of::<INPUT>() as i32);
// Error ignored, as the operation is not always necessary.
let _ = SetWindowPos(
hwnd,
HWND(as_ptr!(hwnd)),
HWND_TOP,
0,
0,
@@ -505,7 +548,7 @@ impl WindowsApi {
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
)
.process();
SetForegroundWindow(hwnd)
SetForegroundWindow(HWND(as_ptr!(hwnd)))
}
.ok()
.process()
@@ -513,7 +556,7 @@ impl WindowsApi {
#[allow(dead_code)]
pub fn top_window() -> Result<isize> {
unsafe { GetTopWindow(HWND::default()) }.process()
unsafe { GetTopWindow(HWND::default())? }.process()
}
pub fn desktop_window() -> Result<isize> {
@@ -521,8 +564,8 @@ impl WindowsApi {
}
#[allow(dead_code)]
pub fn next_window(hwnd: HWND) -> Result<isize> {
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
pub fn next_window(hwnd: isize) -> Result<isize> {
unsafe { GetWindow(HWND(as_ptr!(hwnd)), GW_HWNDNEXT)? }.process()
}
pub fn alt_tab_windows() -> Result<Vec<Window>> {
@@ -541,17 +584,17 @@ impl WindowsApi {
let mut next_hwnd = hwnd;
while next_hwnd != 0 {
if Self::is_window_visible(HWND(next_hwnd)) {
if Self::is_window_visible(next_hwnd) {
return Ok(next_hwnd);
}
next_hwnd = Self::next_window(HWND(next_hwnd))?;
next_hwnd = Self::next_window(next_hwnd)?;
}
Err(anyhow!("could not find next window"))
}
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
pub fn window_rect(hwnd: isize) -> Result<Rect> {
let mut rect = unsafe { std::mem::zeroed() };
if Self::dwm_get_window_attribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut rect).is_ok() {
@@ -561,7 +604,7 @@ impl WindowsApi {
// Ok(Rect::from(rect).scale(system_scale.try_into()?, window_scale.try_into()?))
Ok(Rect::from(rect))
} else {
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
unsafe { GetWindowRect(HWND(as_ptr!(hwnd)), &mut rect) }.process()?;
Ok(Rect::from(rect))
}
}
@@ -571,7 +614,7 @@ impl WindowsApi {
/// added to a position rect to compute a size for set_window_pos that will
/// fill the target area, ignoring shadows.
fn shadow_rect(hwnd: HWND) -> Result<Rect> {
let window_rect = Self::window_rect(hwnd)?;
let window_rect = Self::window_rect(hwnd.0 as isize)?;
let mut srect = Default::default();
unsafe { GetWindowRect(hwnd, &mut srect) }.process()?;
@@ -587,7 +630,8 @@ impl WindowsApi {
pub fn round_rect(hdc: HDC, rect: &Rect, border_radius: i32) {
unsafe {
RoundRect(
// TODO: error handling
let _ = RoundRect(
hdc,
rect.left,
rect.top,
@@ -600,7 +644,8 @@ impl WindowsApi {
}
pub fn rectangle(hdc: HDC, rect: &Rect) {
unsafe {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
// TODO: error handling
let _ = Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
@@ -626,13 +671,16 @@ impl WindowsApi {
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
}
pub fn window_thread_process_id(hwnd: HWND) -> (u32, u32) {
pub fn window_thread_process_id(hwnd: isize) -> (u32, u32) {
let mut process_id: u32 = 0;
// Behaviour is undefined if an invalid HWND is given
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid
let thread_id = unsafe {
GetWindowThreadProcessId(hwnd, Option::from(std::ptr::addr_of_mut!(process_id)))
GetWindowThreadProcessId(
HWND(as_ptr!(hwnd)),
Option::from(std::ptr::addr_of_mut!(process_id)),
)
};
(process_id, thread_id)
@@ -655,7 +703,7 @@ impl WindowsApi {
}
}
#[allow(dead_code)]
#[cfg(target_pointer_width = "64")]
fn set_window_long_ptr_w(
hwnd: HWND,
index: WINDOW_LONG_PTR_INDEX,
@@ -667,14 +715,39 @@ impl WindowsApi {
.map(|_| {})
}
pub fn gwl_style(hwnd: HWND) -> Result<isize> {
Self::window_long_ptr_w(hwnd, GWL_STYLE)
#[cfg(target_pointer_width = "32")]
fn set_window_long_ptr_w(
hwnd: HWND,
index: WINDOW_LONG_PTR_INDEX,
new_value: i32,
) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
SetWindowLongPtrW(hwnd, index, new_value)
}))
.map(|_| {})
}
pub fn gwl_ex_style(hwnd: HWND) -> Result<isize> {
Self::window_long_ptr_w(hwnd, GWL_EXSTYLE)
#[cfg(target_pointer_width = "64")]
pub fn gwl_style(hwnd: isize) -> Result<isize> {
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE)
}
#[cfg(target_pointer_width = "32")]
pub fn gwl_style(hwnd: isize) -> Result<i32> {
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE)
}
#[cfg(target_pointer_width = "64")]
pub fn gwl_ex_style(hwnd: isize) -> Result<isize> {
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE)
}
#[cfg(target_pointer_width = "32")]
pub fn gwl_ex_style(hwnd: isize) -> Result<i32> {
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE)
}
#[cfg(target_pointer_width = "64")]
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<isize> {
// Can return 0, which does not always mean that an error has occurred
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
@@ -683,19 +756,38 @@ impl WindowsApi {
}))
}
#[allow(dead_code)]
pub fn update_style(hwnd: HWND, new_value: isize) -> Result<()> {
Self::set_window_long_ptr_w(hwnd, GWL_STYLE, new_value)
#[cfg(target_pointer_width = "32")]
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<i32> {
// Can return 0, which does not always mean that an error has occurred
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
Result::from(WindowsResult::from(unsafe {
GetWindowLongPtrW(hwnd, index)
}))
}
#[allow(dead_code)]
pub fn update_ex_style(hwnd: HWND, new_value: isize) -> Result<()> {
Self::set_window_long_ptr_w(hwnd, GWL_EXSTYLE, new_value)
#[cfg(target_pointer_width = "64")]
pub fn update_style(hwnd: isize, new_value: isize) -> Result<()> {
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE, new_value)
}
pub fn window_text_w(hwnd: HWND) -> Result<String> {
#[cfg(target_pointer_width = "32")]
pub fn update_style(hwnd: isize, new_value: i32) -> Result<()> {
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE, new_value)
}
#[cfg(target_pointer_width = "64")]
pub fn update_ex_style(hwnd: isize, new_value: isize) -> Result<()> {
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE, new_value)
}
#[cfg(target_pointer_width = "32")]
pub fn update_ex_style(hwnd: isize, new_value: i32) -> Result<()> {
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE, new_value)
}
pub fn window_text_w(hwnd: isize) -> Result<String> {
let mut text: [u16; 512] = [0; 512];
match WindowsResult::from(unsafe { GetWindowTextW(hwnd, &mut text) }) {
match WindowsResult::from(unsafe { GetWindowTextW(HWND(as_ptr!(hwnd)), &mut text) }) {
WindowsResult::Ok(len) => {
let length = usize::try_from(len)?;
Ok(String::from_utf16(&text[..length])?)
@@ -741,25 +833,25 @@ impl WindowsApi {
.to_string())
}
pub fn real_window_class_w(hwnd: HWND) -> Result<String> {
pub fn real_window_class_w(hwnd: isize) -> Result<String> {
const BUF_SIZE: usize = 512;
let mut class: [u16; BUF_SIZE] = [0; BUF_SIZE];
let len = Result::from(WindowsResult::from(unsafe {
RealGetWindowClassW(hwnd, &mut class)
RealGetWindowClassW(HWND(as_ptr!(hwnd)), &mut class)
}))?;
Ok(String::from_utf16(&class[0..len as usize])?)
}
pub fn dwm_get_window_attribute<T>(
hwnd: HWND,
hwnd: isize,
attribute: DWMWINDOWATTRIBUTE,
value: &mut T,
) -> Result<()> {
unsafe {
DwmGetWindowAttribute(
hwnd,
HWND(as_ptr!(hwnd)),
attribute,
(value as *mut T).cast(),
u32::try_from(std::mem::size_of::<T>())?,
@@ -769,7 +861,7 @@ impl WindowsApi {
Ok(())
}
pub fn is_window_cloaked(hwnd: HWND) -> Result<bool> {
pub fn is_window_cloaked(hwnd: isize) -> Result<bool> {
let mut cloaked: u32 = 0;
Self::dwm_get_window_attribute(hwnd, DWMWA_CLOAKED, &mut cloaked)?;
@@ -779,20 +871,20 @@ impl WindowsApi {
))
}
pub fn is_window(hwnd: HWND) -> bool {
unsafe { IsWindow(hwnd) }.into()
pub fn is_window(hwnd: isize) -> bool {
unsafe { IsWindow(HWND(as_ptr!(hwnd))) }.into()
}
pub fn is_window_visible(hwnd: HWND) -> bool {
unsafe { IsWindowVisible(hwnd) }.into()
pub fn is_window_visible(hwnd: isize) -> bool {
unsafe { IsWindowVisible(HWND(as_ptr!(hwnd))) }.into()
}
pub fn is_iconic(hwnd: HWND) -> bool {
unsafe { IsIconic(hwnd) }.into()
pub fn is_iconic(hwnd: isize) -> bool {
unsafe { IsIconic(HWND(as_ptr!(hwnd))) }.into()
}
pub fn is_zoomed(hwnd: HWND) -> bool {
unsafe { IsZoomed(hwnd) }.into()
pub fn is_zoomed(hwnd: isize) -> bool {
unsafe { IsZoomed(HWND(as_ptr!(hwnd))) }.into()
}
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFOEXW> {
@@ -809,11 +901,17 @@ impl WindowsApi {
for display in win32_display_data::connected_displays_all().flatten() {
if display.hmonitor == hmonitor {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let (device, device_id) = if path.is_empty() {
(String::from("UNKNOWN"), String::from("UNKNOWN"))
} else {
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
(device, device_id)
};
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
@@ -938,7 +1036,7 @@ impl WindowsApi {
unsafe {
GetDpiForMonitor(
HMONITOR(hmonitor),
HMONITOR(as_ptr!(hmonitor)),
MDT_EFFECTIVE_DPI,
std::ptr::addr_of_mut!(dpi_x),
std::ptr::addr_of_mut!(dpi_y),
@@ -962,7 +1060,7 @@ impl WindowsApi {
unsafe {
DwmSetWindowAttribute(
HWND(hwnd),
HWND(as_ptr!(hwnd)),
DWMWA_WINDOW_CORNER_PREFERENCE,
std::ptr::addr_of!(round).cast(),
4,
@@ -975,7 +1073,7 @@ impl WindowsApi {
let col_ref = COLORREF(color.unwrap_or(DWMWA_COLOR_NONE));
unsafe {
DwmSetWindowAttribute(
HWND(hwnd),
HWND(as_ptr!(hwnd)),
DWMWA_BORDER_COLOR,
std::ptr::addr_of!(col_ref).cast(),
4,
@@ -984,7 +1082,7 @@ impl WindowsApi {
.process()
}
pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
pub fn create_border_window(name: PCWSTR, instance: isize) -> Result<isize> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
@@ -997,9 +1095,9 @@ impl WindowsApi {
CW_USEDEFAULT,
None,
None,
instance,
HINSTANCE(as_ptr!(instance)),
None,
);
)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
@@ -1008,16 +1106,21 @@ impl WindowsApi {
.process()
}
pub fn set_transparent(hwnd: HWND, alpha: u8) -> Result<()> {
pub fn set_transparent(hwnd: isize, alpha: u8) -> Result<()> {
unsafe {
#[allow(clippy::cast_sign_loss)]
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), alpha, LWA_ALPHA)?;
SetLayeredWindowAttributes(
HWND(as_ptr!(hwnd)),
COLORREF(-1i32 as u32),
alpha,
LWA_ALPHA,
)?;
}
Ok(())
}
pub fn create_hidden_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> Result<isize> {
unsafe {
CreateWindowExW(
WS_EX_NOACTIVATE,
@@ -1030,16 +1133,16 @@ impl WindowsApi {
CW_USEDEFAULT,
None,
None,
instance,
HINSTANCE(as_ptr!(instance)),
None,
)
)?
}
.process()
}
pub fn invalidate_rect(hwnd: HWND, rect: Option<&Rect>, erase: bool) -> bool {
pub fn invalidate_rect(hwnd: isize, rect: Option<&Rect>, erase: bool) -> bool {
let rect = rect.map(|rect| &rect.rect() as *const RECT);
unsafe { InvalidateRect(hwnd, rect, erase) }.as_bool()
unsafe { InvalidateRect(HWND(as_ptr!(hwnd)), rect, erase) }.as_bool()
}
pub fn alt_is_pressed() -> bool {
@@ -1093,6 +1196,6 @@ impl WindowsApi {
}
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
unsafe { WTSRegisterSessionNotification(HWND(hwnd), 1) }.process()
unsafe { WTSRegisterSessionNotification(HWND(as_ptr!(hwnd)), 1) }.process()
}
}

View File

@@ -16,10 +16,10 @@ use crate::winevent_listener;
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
let is_visible = WindowsApi::is_window_visible(hwnd);
let is_window = WindowsApi::is_window(hwnd);
let is_minimized = WindowsApi::is_iconic(hwnd);
let is_maximized = WindowsApi::is_zoomed(hwnd);
let is_visible = WindowsApi::is_window_visible(hwnd.0 as isize);
let is_window = WindowsApi::is_window(hwnd.0 as isize);
let is_minimized = WindowsApi::is_iconic(hwnd.0 as isize);
let is_maximized = WindowsApi::is_zoomed(hwnd.0 as isize);
if is_visible && is_window && !is_minimized {
let window = Window::from(hwnd);
@@ -27,7 +27,7 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
if should_manage {
if is_maximized {
WindowsApi::restore_window(hwnd);
WindowsApi::restore_window(window.hwnd);
}
let mut container = Container::default();
@@ -43,9 +43,9 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let is_visible = WindowsApi::is_window_visible(hwnd);
let is_window = WindowsApi::is_window(hwnd);
let is_minimized = WindowsApi::is_iconic(hwnd);
let is_visible = WindowsApi::is_window_visible(hwnd.0 as isize);
let is_window = WindowsApi::is_window(hwnd.0 as isize);
let is_minimized = WindowsApi::is_iconic(hwnd.0 as isize);
if is_visible && is_window && !is_minimized {
let window = Window::from(hwnd);

View File

@@ -39,11 +39,12 @@ pub fn start() {
loop {
unsafe {
if !GetMessageW(&mut msg, HWND(0), 0, 0).as_bool() {
if !GetMessageW(&mut msg, HWND(std::ptr::null_mut()), 0, 0).as_bool() {
tracing::debug!("windows event processing thread shutdown");
break;
};
TranslateMessage(&msg);
// TODO: error handling
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}

View File

@@ -12,13 +12,13 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use komorebi_core::Axis;
use komorebi_core::CustomLayout;
use komorebi_core::CycleDirection;
use komorebi_core::DefaultLayout;
use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use crate::core::Axis;
use crate::core::CustomLayout;
use crate::core::CycleDirection;
use crate::core::DefaultLayout;
use crate::core::Layout;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
@@ -30,6 +30,7 @@ use crate::static_config::WorkspaceConfig;
use crate::window::Window;
use crate::window::WindowDetails;
use crate::windows_api::WindowsApi;
use crate::WindowContainerBehaviour;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
@@ -83,6 +84,10 @@ pub struct Workspace {
tile: bool,
#[getset(get_copy = "pub", set = "pub")]
apply_window_based_work_area_offset: bool,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
window_container_behaviour: Option<WindowContainerBehaviour>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
float_override: Option<bool>,
}
impl_ring_elements!(Workspace, Container);
@@ -106,6 +111,8 @@ impl Default for Workspace {
resize_dimensions: vec![],
tile: true,
apply_window_based_work_area_offset: true,
window_container_behaviour: None,
float_override: None,
}
}
}
@@ -162,6 +169,14 @@ impl Workspace {
config.apply_window_based_work_area_offset.unwrap_or(true),
);
if config.window_container_behaviour.is_some() {
self.set_window_container_behaviour(config.window_container_behaviour);
}
if config.float_override.is_some() {
self.set_float_override(config.float_override);
}
Ok(())
}
@@ -217,22 +232,19 @@ impl Workspace {
container.restore();
}
for container in self.containers_mut() {
container.restore();
if let Some(container) = self.focused_container_mut() {
container.focus_window(container.focused_window_idx());
}
for window in self.floating_windows() {
window.restore();
}
if let Some(container) = self.focused_container_mut() {
container.focus_window(container.focused_window_idx());
}
// Do this here to make sure that an error doesn't stop the restoration of other windows
// Maximised windows should always be drawn at the top of the Z order
// Maximised windows and floating windows should always be drawn at the top of the Z order
// when switching to a workspace
if let Some(window) = to_focus {
if self.maximized_window().is_none() {
if self.maximized_window().is_none() && self.floating_windows().is_empty() {
window.focus(mouse_follows_focus)?;
}
}
@@ -267,7 +279,8 @@ impl Workspace {
},
);
if self.containers().len() <= window_based_work_area_offset_limit as usize
if (self.containers().len() <= window_based_work_area_offset_limit as usize
|| self.monocle_container().is_some() && window_based_work_area_offset_limit > 0)
&& self.apply_window_based_work_area_offset
{
adjusted_work_area = window_based_work_area_offset.map_or_else(
@@ -356,7 +369,7 @@ impl Workspace {
// If a window has been unmaximized via toggle-maximize, this block
// will make sure that it is unmaximized via restore_window
if window.is_maximized() && !managed_maximized_window {
WindowsApi::restore_window(window.hwnd());
WindowsApi::restore_window(window.hwnd);
}
{
@@ -392,26 +405,6 @@ impl Workspace {
Ok(())
}
// focus_changed performs updates in response to the fact that a focus
// change event has occurred. The focus change is assumed to be valid, and
// should not result in a new focus change - the intent here is to update
// focus-reactive elements, such as the stackbar.
pub fn focus_changed(&mut self, hwnd: isize) -> Result<()> {
if !self.tile() {
return Ok(());
}
let containers = self.containers_mut();
for container in containers.iter_mut() {
if let Some(idx) = container.idx_for_window(hwnd) {
container.focus_window(idx);
container.restore();
}
}
Ok(())
}
pub fn reap_orphans(&mut self) -> Result<(usize, usize)> {
let mut hwnds = vec![];
let mut floating_hwnds = vec![];
@@ -604,6 +597,13 @@ impl Workspace {
Ok(false)
}
pub fn is_empty(&self) -> bool {
self.containers().is_empty()
&& self.maximized_window().is_none()
&& self.monocle_container().is_none()
&& self.floating_windows().is_empty()
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {
@@ -655,13 +655,19 @@ impl Workspace {
Ok(())
}
pub fn add_container(&mut self, container: Container) {
pub fn add_container_to_back(&mut self, container: Container) {
self.containers_mut().push_back(container);
self.focus_last_container();
}
pub fn add_container_to_front(&mut self, container: Container) {
self.containers_mut().push_front(container);
self.focus_first_container();
}
pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) {
self.containers_mut().insert(idx, container);
self.focus_container(idx);
}
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
@@ -1441,4 +1447,8 @@ impl Workspace {
fn focus_last_container(&mut self) {
self.focus_container(self.containers().len().saturating_sub(1));
}
fn focus_first_container(&mut self) {
self.focus_container(0);
}
}

View File

@@ -118,7 +118,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Unblock the border manager
ALT_TAB_HWND.store(None);
// Send a notification to the border manager to update the borders
border_manager::send_notification();
border_manager::send_notification(None);
}
}
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.28-dev.0"
version = "0.1.30"
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"]
@@ -11,31 +11,34 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-core = { path = "../komorebi-core" }
komorebi-client = { path = "../komorebi-client" }
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono = "0.4"
chrono = { workspace = true }
clap = { workspace = true }
color-eyre = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
fs-tail = "0.1"
lazy_static = "1"
lazy_static = { workspace = true }
miette = { version = "7", features = ["fancy"] }
paste = "1"
paste = { workspace = true }
powershell_script = "1.0"
reqwest = { version = "0.12", features = ["blocking"] }
serde = { version = "1", features = ["derive"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
shadow-rs = { workspace = true }
sysinfo = { workspace = true }
thiserror = "1"
uds_windows = "1"
which = "6"
uds_windows = { workspace = true }
which = { workspace = true }
win32-display-data = { workspace = true }
windows = { workspace = true }
shadow-rs = { workspace = true }
[build-dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
shadow-rs = { workspace = true }
shadow-rs = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }

File diff suppressed because it is too large Load Diff

View File

@@ -73,12 +73,14 @@ nav:
- Release notes:
- release/v0-1-22.md
- Configuration reference: https://komorebi.lgug2z.com/schema
- Bar reference: https://komorebi-bar.lgug2z.com/schema
- CLI reference:
- cli/quickstart.md
- cli/start.md
- cli/stop.md
- cli/check.md
- cli/configuration.md
- cli/bar-configuration.md
- cli/whkdrc.md
- cli/state.md
- cli/global-state.md
@@ -103,12 +105,13 @@ nav:
- cli/cycle-focus.md
- cli/cycle-move.md
- cli/stack.md
- cli/unstack.md
- cli/cycle-stack.md
- cli/focus-stack-window.md
- cli/stack-all.md
- cli/unstack-all.md
- cli/resize-edge.md
- cli/resize-axis.md
- cli/unstack.md
- cli/cycle-stack.md
- cli/move-to-monitor.md
- cli/cycle-move-to-monitor.md
- cli/move-to-workspace.md
@@ -179,6 +182,7 @@ nav:
- cli/restore-windows.md
- cli/manage.md
- cli/unmanage.md
- cli/replace-configuration.md
- cli/reload-configuration.md
- cli/watch-configuration.md
- cli/complete-configuration.md
@@ -208,6 +212,11 @@ nav:
- cli/border-implementation.md
- cli/transparency.md
- cli/transparency-alpha.md
- cli/toggle-transparency.md
- cli/animation.md
- cli/animation-duration.md
- cli/animation-fps.md
- cli/animation-style.md
- cli/focus-follows-mouse.md
- cli/toggle-focus-follows-mouse.md
- cli/mouse-follows-focus.md

View File

@@ -1,44 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Array_of_ApplicationConfiguration",
"type": "array",
"items": {
"$ref": "#/definitions/ApplicationConfiguration"
"title": "ApplicationSpecificConfiguration",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/AscApplicationRulesOrSchema"
},
"definitions": {
"ApplicationConfiguration": {
"type": "object",
"required": [
"identifier",
"name"
],
"properties": {
"float_identifiers": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/IdWithIdentifierAndComment"
}
},
"identifier": {
"$ref": "#/definitions/IdWithIdentifier"
},
"name": {
"type": "string"
},
"options": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/ApplicationOptions"
}
}
}
},
"ApplicationIdentifier": {
"type": "string",
"enum": [
@@ -48,14 +15,100 @@
"Path"
]
},
"ApplicationOptions": {
"type": "string",
"enum": [
"object_name_change",
"layered",
"border_overflow",
"tray_and_multi_window",
"force"
"AscApplicationRules": {
"description": "Rules that determine how an application is handled",
"type": "object",
"properties": {
"floating": {
"description": "Rules to manage specific windows as floating windows",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
},
"ignore": {
"description": "Rules to ignore specific windows",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
},
"layered": {
"description": "Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
},
"manage": {
"description": "Rules to forcibly manage specific windows",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
},
"object_name_change": {
"description": "Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
},
"slow_application": {
"description": "Rules to identify applications which are slow to send initial event notifications",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
},
"transparency_ignore": {
"description": "Rules to ignore specific windows from the transparency feature",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
},
"tray_and_multi_window": {
"description": "Rules to identify applications which minimize to the tray or have multiple windows",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/MatchingRule"
}
}
}
},
"AscApplicationRulesOrSchema": {
"anyOf": [
{
"$ref": "#/definitions/AscApplicationRules"
},
{
"type": "string"
}
]
},
"IdWithIdentifier": {
@@ -83,36 +136,18 @@
}
}
},
"IdWithIdentifierAndComment": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"comment": {
"type": [
"string",
"null"
]
"MatchingRule": {
"anyOf": [
{
"$ref": "#/definitions/IdWithIdentifier"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/ApplicationIdentifier"
},
"matching_strategy": {
"anyOf": [
{
"$ref": "#/definitions/MatchingStrategy"
},
{
"type": "null"
}
]
{
"type": "array",
"items": {
"$ref": "#/definitions/IdWithIdentifier"
}
}
}
]
},
"MatchingStrategy": {
"type": "string",
@@ -122,7 +157,11 @@
"StartsWith",
"EndsWith",
"Contains",
"Regex"
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}

Some files were not shown because too many files have changed in this diff Show More