Compare commits

..

66 Commits

Author SHA1 Message Date
LGUG2Z
261daedcf5 fix(borders): send notif on ignored hwnd events 2024-10-02 08:59:38 -07:00
LGUG2Z
bd24940173 fix(borders): update on floating win drag 2024-10-01 09:21:01 -07:00
LGUG2Z
9034122447 fix(borders): no redraws on floating win title change
WIP
2024-10-01 09:21:01 -07:00
LGUG2Z
105957b5fa refactor(wm): float_rules > ignore_rules w/ compat
WIP
2024-10-01 09:21:01 -07:00
LGUG2Z
bf5b675498 feat(wm): allow ws moves of floating apps
WIP
2024-10-01 09:21:01 -07:00
LGUG2Z
8c8d1175ba feat(wm): separate floating and ignored apps
WIP
2024-10-01 09:21:01 -07:00
LGUG2Z
bf2b73837b 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-01 08:54:18 -07:00
LGUG2Z
04cde3f757 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.
2024-09-29 18:10:50 -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
96 changed files with 9042 additions and 1865 deletions

View File

@@ -95,6 +95,7 @@ jobs:
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-bar.exe
target/${{ matrix.target }}/release/komorebi-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
@@ -104,7 +105,7 @@ jobs:
- name: Check GoReleaser
uses: goreleaser/goreleaser-action@v3
env:
GORELEASER_CURRENT_TAG: v0.1.28
GORELEASER_CURRENT_TAG: v0.1.29
with:
version: latest
args: build --skip=validate --clean
@@ -134,6 +135,7 @@ jobs:
shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly
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

1
.gitignore vendored
View File

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

View File

@@ -44,6 +44,15 @@ builds:
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"
- id: komorebi-bar
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-bar
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-bar.exe" ".\dist\komorebi-bar_windows_amd64_v1\komorebi-bar.exe"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"

1949
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.28"
egui_extras = "0.28"
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

@@ -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

@@ -19,6 +19,8 @@ install-target target:
install:
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi-gui
just install-target komorebi-bar
just install-target komorebi
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

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

@@ -0,0 +1,36 @@
[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.6.0"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.31"
num = "0.4.3"
num-derive = "0.4.2"
num-traits = "0.2.19"
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" }

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

@@ -0,0 +1,422 @@
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::MAX_LABEL_WIDTH;
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 eframe::egui::Vec2;
use eframe::egui::ViewportCommand;
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);
}
if let Some(viewport) = &config.viewport {
if let Some(inner_size) = viewport.inner_size {
let mut vec2 = Vec2::new(inner_size.x, inner_size.y * 2.0);
if self.scale_factor != 1.0 {
vec2 = Vec2::new(inner_size.x / self.scale_factor, inner_size.y * 2.0);
}
ctx.send_viewport_cmd(ViewportCommand::InnerSize(vec2));
}
}
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);
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,
}

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

@@ -0,0 +1,141 @@
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>,
}
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;
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 = format!("{percentage}%");
}
}
Self {
enable: value.enable,
manager,
last_state,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
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,
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 = 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(
emoji.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()),
);
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
}
ui.add_space(WIDGET_SPACING);
}
}
}

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

@@ -0,0 +1,133 @@
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::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
pub struct KomobarConfig {
/// Viewport options (see: https://docs.rs/egui/latest/egui/viewport/struct.ViewportBuilder.html)
pub viewport: Option<ViewportConfig>,
/// 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>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ViewportConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
pub position: Option<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
pub inner_size: 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,
},
}
}
}

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

@@ -0,0 +1,123 @@
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,
}
impl From<DateConfig> for Date {
fn from(value: DateConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
}
}
}
#[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,
}
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 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::CALENDAR_DOTS.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()),
);
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,538 @@
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 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() {
if let Ok(expanded) = std::env::var("KOMOREBI_CONFIG_HOME") {
*location = location.replace("$Env:KOMOREBI_CONFIG_HOME", &expanded);
}
if let Ok(expanded) = std::env::var("USERPROFILE") {
*location = location.replace("$Env:USERPROFILE", &expanded);
}
*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::Retile).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>>,
) {
if let Ok(notification) = rx_gui.try_recv() {
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;
}
}
}
}

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

@@ -0,0 +1,318 @@
mod bar;
mod battery;
mod config;
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 clap::Parser;
use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::SocketMessage;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
pub static WIDGET_SPACING: f32 = 10.0;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
#[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,
}
fn main() -> color_eyre::Result<()> {
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 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) => {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
KomobarConfig::read(config)?
}
};
let config_path = config_path.unwrap_or(default_config_path);
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
&SocketMessage::State,
)?)?;
let mut viewport_builder = ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_taskbar(false)
.with_position(Position { x: 0.0, y: 0.0 })
.with_inner_size({
Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 20.0,
}
});
if let Some(viewport) = &config.viewport {
if let Some(position) = &viewport.position {
let b = viewport_builder.clone();
viewport_builder = b.with_position(*position);
}
if let Some(inner_size) = &viewport.inner_size {
let b = viewport_builder.clone();
viewport_builder = b.with_inner_size(*inner_size);
}
}
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 listener = komorebi_client::subscribe("komorebi-bar")
.expect("could not subscribe to komorebi notifications");
tracing::info!("subscribed to komorebi notifications: \"komorebi-bar\"");
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(String::from(
"komorebi-bar",
)),
)
.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) => {
if let Ok(notification) =
serde_json::from_str::<komorebi_client::Notification>(
&notification_string,
)
{
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(notification) {
tracing::error!("could not send komorebi notification update to gui: {error}")
}
ctx_komorebi.request_repaint();
}
}
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);
}
}
}
}

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

@@ -0,0 +1,108 @@
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>,
}
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),
last_updated: Instant::now(),
}
}
}
pub struct Memory {
pub enable: bool,
system: System,
data_refresh_interval: u64,
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();
format!("RAM: {}%", (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(
egui_phosphor::regular::MEMORY.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()),
);
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);
}
}
}

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

@@ -0,0 +1,297 @@
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>,
}
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();
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(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),
))
}
}
}
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(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(),
))
}
}
}
}
}
Self {
enable: value.enable,
networks_total_data_transmitted,
networks_network_activity,
default_interface,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
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,
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(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,
))
}
}
}
}
}
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(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),
))
}
}
}
}
}
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(
egui_phosphor::regular::WIFI_HIGH.to_string(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
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"),
}
}

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

@@ -0,0 +1,123 @@
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>,
}
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),
last_updated: Instant::now(),
}
}
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
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(format!(
"{} {}%",
mount.to_string_lossy(),
(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(
egui_phosphor::regular::HARD_DRIVES.to_string(),
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);
}
}
}
}

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

@@ -0,0 +1,114 @@
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,
}
impl From<TimeConfig> for Time {
fn from(value: TimeConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
}
}
}
#[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,
}
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 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::CLOCK.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()),
);
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,52 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
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),
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::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

@@ -3,7 +3,33 @@
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,6 +37,7 @@ 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;
@@ -18,20 +45,6 @@ pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
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 +59,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);

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

@@ -218,7 +218,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()

View File

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

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

@@ -0,0 +1,158 @@
#![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,
}
impl CatppuccinValue {
pub fn color32(&self, theme: catppuccin_egui::Theme) -> Color32 {
match self {
CatppuccinValue::Rosewater => theme.rosewater,
CatppuccinValue::Flamingo => theme.flamingo,
CatppuccinValue::Pink => theme.pink,
CatppuccinValue::Mauve => theme.mauve,
CatppuccinValue::Red => theme.red,
CatppuccinValue::Maroon => theme.maroon,
CatppuccinValue::Peach => theme.peach,
CatppuccinValue::Yellow => theme.yellow,
CatppuccinValue::Green => theme.green,
CatppuccinValue::Teal => theme.teal,
CatppuccinValue::Sky => theme.sky,
CatppuccinValue::Sapphire => theme.sapphire,
CatppuccinValue::Blue => theme.blue,
CatppuccinValue::Lavender => theme.lavender,
CatppuccinValue::Text => theme.text,
CatppuccinValue::Subtext1 => theme.subtext1,
CatppuccinValue::Subtext0 => theme.subtext0,
CatppuccinValue::Overlay2 => theme.overlay2,
CatppuccinValue::Overlay1 => theme.overlay1,
CatppuccinValue::Overlay0 => theme.overlay0,
CatppuccinValue::Surface2 => theme.surface2,
CatppuccinValue::Surface1 => theme.surface1,
CatppuccinValue::Surface0 => theme.surface0,
CatppuccinValue::Base => theme.base,
CatppuccinValue::Mantle => theme.mantle,
CatppuccinValue::Crust => theme.crust,
}
}
}

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

@@ -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(

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

@@ -45,12 +45,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),
@@ -132,6 +133,7 @@ pub enum SocketMessage {
ClearNamedWorkspaceLayoutRules(String),
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
ReloadStaticConfiguration(PathBuf),
WatchConfiguration(bool),
CompleteConfiguration,
@@ -150,6 +152,7 @@ pub enum SocketMessage {
BorderOffset(i32),
BorderImplementation(BorderImplementation),
Transparency(bool),
ToggleTransparency,
TransparencyAlpha(u8),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
@@ -171,7 +174,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),
@@ -291,6 +295,7 @@ pub enum WindowKind {
Stack,
Monocle,
Unfocused,
Floating,
}
#[derive(
@@ -359,6 +364,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,
)]

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(),
]));
@@ -166,7 +168,7 @@ lazy_static! {
]));
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()));
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -216,7 +218,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,7 +229,6 @@ 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);
#[must_use]

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;
@@ -172,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 {}",
@@ -219,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 {}",
@@ -319,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"
@@ -406,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,6 +57,7 @@ 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;
@@ -65,8 +68,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;
@@ -79,7 +82,7 @@ use crate::SUBSCRIPTION_SOCKETS;
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;
@@ -228,9 +231,13 @@ impl WindowManager {
self.cycle_container_window_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::FocusStackWindow(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 +270,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 +387,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 +486,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 +502,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 +520,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 +536,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)?;
@@ -775,6 +825,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 +1142,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)?;
}
@@ -1301,7 +1388,7 @@ impl WindowManager {
}
}
border_manager::send_notification();
border_manager::send_notification(None);
}
}
SocketMessage::BorderColour(kind, r, g, b) => match kind {
@@ -1317,6 +1404,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 +1429,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);
}
@@ -1442,58 +1536,13 @@ impl WindowManager {
};
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<()> {

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;
@@ -33,6 +33,7 @@ use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
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 +64,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 +102,10 @@ impl WindowManager {
}
if !transparency_override {
if rule_debug.matches_ignore_identifier.is_some() {
border_manager::send_notification(Option::from(event.hwnd()));
}
return Ok(());
}
}
@@ -149,14 +154,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 +243,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)?;
}
}
}
@@ -336,19 +340,44 @@ impl WindowManager {
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();
}
}
if should_float && !matches!(event, WindowManagerEvent::Manage(_)) {
workspace.floating_windows_mut().push(window);
self.update_focused_workspace(false, true)?;
} else {
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)?;
stackbar_manager::send_notification();
}
}
}
}
@@ -383,7 +412,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));
@@ -407,7 +436,7 @@ impl WindowManager {
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)
@@ -581,14 +610,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));
}
@@ -637,7 +671,7 @@ impl WindowManager {
};
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();

View File

@@ -4,6 +4,9 @@ use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::colour::Colour;
use crate::core::BorderImplementation;
use crate::core::StackbarLabel;
use crate::core::StackbarMode;
use crate::current_virtual_desktop;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
@@ -23,6 +26,7 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::CrossBoundaryBehaviour;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
@@ -31,43 +35,41 @@ use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::FLOATING_APPLICATIONS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::TRANSPARENCY_BLACKLIST;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
use crate::WORKSPACE_RULES;
use komorebi_core::BorderImplementation;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use crate::WORKSPACE_MATCHING_RULES;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::ApplicationConfigurationGenerator;
use crate::core::config_generation::ApplicationOptions;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::resolve_home_path;
use crate::core::AnimationStyle;
use crate::core::BorderStyle;
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::Rect;
use crate::core::SocketMessage;
use crate::core::WindowContainerBehaviour;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_core::config_generation::ApplicationConfiguration;
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
use komorebi_core::config_generation::ApplicationOptions;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::resolve_home_path;
use komorebi_core::AnimationStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::BorderStyle;
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::Rect;
use komorebi_core::SocketMessage;
use komorebi_core::WindowContainerBehaviour;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
@@ -119,10 +121,10 @@ pub struct WorkspaceConfig {
pub workspace_padding: Option<i32>,
/// Initial workspace application rules
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_workspace_rules: Option<Vec<IdWithIdentifier>>,
pub initial_workspace_rules: Option<Vec<MatchingRule>>,
/// Permanent workspace application rules
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_rules: Option<Vec<IdWithIdentifier>>,
pub workspace_rules: Option<Vec<MatchingRule>>,
/// Apply this monitor's window-based work area offset (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub apply_window_based_work_area_offset: Option<bool>,
@@ -140,37 +142,6 @@ impl From<&Workspace> for WorkspaceConfig {
}
}
let workspace_rules = WORKSPACE_RULES.lock();
let mut initial_ws_rules = vec![];
let mut ws_rules = vec![];
for (identifier, (_, _, is_initial)) in &*workspace_rules {
if identifier.ends_with("exe") {
let rule = IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: identifier.clone(),
matching_strategy: None,
};
if *is_initial {
initial_ws_rules.push(rule);
} else {
ws_rules.push(rule);
}
}
}
let initial_ws_rules = if initial_ws_rules.is_empty() {
None
} else {
Option::from(initial_ws_rules)
};
let ws_rules = if ws_rules.is_empty() {
None
} else {
Option::from(ws_rules)
};
let default_container_padding = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
let default_workspace_padding = DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst);
@@ -206,8 +177,8 @@ impl From<&Workspace> for WorkspaceConfig {
custom_layout_rules: None,
container_padding,
workspace_padding,
initial_workspace_rules: initial_ws_rules,
workspace_rules: ws_rules,
initial_workspace_rules: None,
workspace_rules: None,
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
}
}
@@ -245,7 +216,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.28`
/// The `komorebi.json` static configuration file reference for `v0.1.30`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -265,6 +236,9 @@ pub struct StaticConfig {
/// Determine what happens when a window is moved across a monitor boundary (default: Swap)
#[serde(skip_serializing_if = "Option::is_none")]
pub cross_monitor_move_behaviour: Option<MoveBehaviour>,
/// Determine what happens when an action is called on a window at a monitor boundary (default: Monitor)
#[serde(skip_serializing_if = "Option::is_none")]
pub cross_boundary_behaviour: Option<CrossBoundaryBehaviour>,
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
#[serde(skip_serializing_if = "Option::is_none")]
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
@@ -300,7 +274,7 @@ pub struct StaticConfig {
/// Active window border z-order (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>,
/// Display an active window border (default: false)
/// Active window border implementation (default: Komorebi)
#[serde(skip_serializing_if = "Option::is_none")]
pub border_implementation: Option<BorderImplementation>,
/// Add transparency to unfocused windows (default: false)
@@ -309,6 +283,9 @@ pub struct StaticConfig {
/// Alpha value for unfocused window transparency [[0-255]] (default: 200)
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency_alpha: Option<u8>,
/// Individual window transparency ignore rules
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency_ignore_rules: Option<Vec<MatchingRule>>,
/// Global default workspace padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
pub default_workspace_padding: Option<i32>,
@@ -326,10 +303,14 @@ pub struct StaticConfig {
pub global_work_area_offset: Option<Rect>,
/// Individual window floating rules
#[serde(skip_serializing_if = "Option::is_none")]
pub float_rules: Option<Vec<MatchingRule>>,
#[serde(alias = "float_rules")]
pub ignore_rules: Option<Vec<MatchingRule>>,
/// Individual window force-manage rules
#[serde(skip_serializing_if = "Option::is_none")]
pub manage_rules: Option<Vec<MatchingRule>>,
/// Identify applications which should be managed as floating windows
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_applications: Option<Vec<MatchingRule>>,
/// Identify border overflow applications
#[serde(skip_serializing_if = "Option::is_none")]
pub border_overflow_applications: Option<Vec<MatchingRule>>,
@@ -354,6 +335,9 @@ pub struct StaticConfig {
/// Animations configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub animation: Option<AnimationsConfig>,
/// Theme configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<KomorebiTheme>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -367,6 +351,56 @@ pub struct AnimationsConfig {
/// Set the animation FPS (default: 60)
fps: Option<u64>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "palette")]
pub enum KomorebiTheme {
/// A theme from catppuccin-egui
Catppuccin {
/// Name of the Catppuccin theme
name: komorebi_themes::Catppuccin,
/// Border colour when the container contains a single window (default: Blue)
single_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container contains multiple windows (default: Green)
stack_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is in monocle mode (default: Pink)
monocle_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the window is floating (default: Yellow)
floating_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused (default: Base)
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar focused tab text colour (default: Green)
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar unfocused tab text colour (default: Text)
stackbar_unfocused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar tab background colour (default: Base)
stackbar_background: Option<komorebi_themes::CatppuccinValue>,
/// Komorebi status bar accent (default: Blue)
bar_accent: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme
name: komorebi_themes::Base16,
/// Border colour when the container contains a single window (default: Base0D)
single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
bar_accent: Option<komorebi_themes::Base16Value>,
},
}
impl StaticConfig {
pub fn aliases(raw: &str) {
@@ -442,7 +476,7 @@ pub struct TabsConfig {
pub struct StackbarConfig {
/// Stackbar height
pub height: Option<i32>,
/// Stackbar height
/// Stackbar label
pub label: Option<StackbarLabel>,
/// Stackbar mode
pub mode: Option<StackbarMode>,
@@ -458,95 +492,6 @@ impl From<&WindowManager> for StaticConfig {
monitors.push(MonitorConfig::from(m));
}
let mut to_remove = vec![];
let mut to_add_initial = vec![];
let mut to_add_persistent = vec![];
let workspace_rules = WORKSPACE_RULES.lock();
for (m_idx, m) in monitors.iter().enumerate() {
for (w_idx, w) in m.workspaces.iter().enumerate() {
if let Some(rules) = &w.initial_workspace_rules {
for iwsr in rules {
for (identifier, (monitor_idx, workspace_idx, _)) in &*workspace_rules {
if iwsr.id.eq(identifier)
&& (*monitor_idx != m_idx || *workspace_idx != w_idx)
{
to_remove.push((m_idx, w_idx, iwsr.id.clone()));
}
}
}
}
for (identifier, (monitor_idx, workspace_idx, initial)) in &*workspace_rules {
if *initial && (*monitor_idx == m_idx && *workspace_idx == w_idx) {
to_add_initial.push((m_idx, w_idx, identifier.clone()));
}
}
if let Some(rules) = &w.workspace_rules {
for wsr in rules {
for (identifier, (monitor_idx, workspace_idx, _)) in &*workspace_rules {
if wsr.id.eq(identifier)
&& (*monitor_idx != m_idx || *workspace_idx != w_idx)
{
to_remove.push((m_idx, w_idx, wsr.id.clone()));
}
}
}
}
for (identifier, (monitor_idx, workspace_idx, initial)) in &*workspace_rules {
if !*initial && (*monitor_idx == m_idx && *workspace_idx == w_idx) {
to_add_persistent.push((m_idx, w_idx, identifier.clone()));
}
}
}
}
for (m_idx, w_idx, id) in to_remove {
if let Some(monitor) = monitors.get_mut(m_idx) {
if let Some(workspace) = monitor.workspaces.get_mut(w_idx) {
if workspace.workspace_rules.is_none() {
workspace.workspace_rules = Some(vec![]);
}
if let Some(rules) = &mut workspace.workspace_rules {
rules.retain(|r| r.id != id);
for (monitor_idx, workspace_idx, id) in &to_add_persistent {
if m_idx == *monitor_idx && w_idx == *workspace_idx {
rules.push(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: id.clone(),
matching_strategy: None,
})
}
}
rules.dedup();
}
if workspace.initial_workspace_rules.is_none() {
workspace.workspace_rules = Some(vec![]);
}
if let Some(rules) = &mut workspace.initial_workspace_rules {
rules.retain(|r| r.id != id);
for (monitor_idx, workspace_idx, id) in &to_add_initial {
if m_idx == *monitor_idx && w_idx == *workspace_idx {
rules.push(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: id.clone(),
matching_strategy: None,
})
}
}
rules.dedup();
}
}
}
}
let border_colours = if border_manager::FOCUSED.load(Ordering::SeqCst) == 0 {
None
} else {
@@ -565,6 +510,7 @@ impl From<&WindowManager> for StaticConfig {
resize_delta: Option::from(value.resize_delta),
window_container_behaviour: Option::from(value.window_container_behaviour),
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
cross_boundary_behaviour: Option::from(value.cross_boundary_behaviour),
unmanaged_window_operation_behaviour: Option::from(
value.unmanaged_window_operation_behaviour,
),
@@ -583,6 +529,7 @@ impl From<&WindowManager> for StaticConfig {
transparency_alpha: Option::from(
transparency_manager::TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
),
transparency_ignore_rules: None,
border_style: Option::from(STYLE.load()),
border_z_order: Option::from(Z_ORDER.load()),
border_implementation: Option::from(IMPLEMENTATION.load()),
@@ -595,7 +542,8 @@ impl From<&WindowManager> for StaticConfig {
monitors: Option::from(monitors),
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
global_work_area_offset: value.work_area_offset,
float_rules: None,
ignore_rules: None,
floating_applications: None,
manage_rules: None,
border_overflow_applications: None,
tray_and_multi_window_applications: None,
@@ -605,6 +553,7 @@ impl From<&WindowManager> for StaticConfig {
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
stackbar: None,
animation: None,
theme: None,
}
}
}
@@ -698,7 +647,7 @@ impl StaticConfig {
}
}
border_manager::send_notification();
border_manager::send_notification(None);
}
transparency_manager::TRANSPARENCY_ENABLED
@@ -706,15 +655,21 @@ impl StaticConfig {
transparency_manager::TRANSPARENCY_ALPHA
.store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst);
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
let mut layered_identifiers = LAYERED_WHITELIST.lock();
let mut transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
let mut floating_applications = FLOATING_APPLICATIONS.lock();
if let Some(rules) = &mut self.float_rules {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
if let Some(rules) = &mut self.ignore_rules {
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
}
if let Some(rules) = &mut self.floating_applications {
populate_rules(rules, &mut floating_applications, &mut regex_identifiers)?;
}
if let Some(rules) = &mut self.manage_rules {
@@ -741,6 +696,10 @@ impl StaticConfig {
)?;
}
if let Some(rules) = &mut self.transparency_ignore_rules {
populate_rules(rules, &mut transparency_blacklist, &mut regex_identifiers)?;
}
if let Some(stackbar) = &self.stackbar {
if let Some(height) = &stackbar.height {
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
@@ -777,14 +736,162 @@ impl StaticConfig {
}
}
if let Some(theme) = &self.theme {
let (
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
) = match theme {
KomorebiTheme::Catppuccin {
name,
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
..
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
.color32(name.as_theme());
let stack_border = stack_border
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
.color32(name.as_theme());
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::CatppuccinValue::Pink)
.color32(name.as_theme());
let floating_border = floating_border
.unwrap_or(komorebi_themes::CatppuccinValue::Yellow)
.color32(name.as_theme());
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
.color32(name.as_theme());
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
.color32(name.as_theme());
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::CatppuccinValue::Text)
.color32(name.as_theme());
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
.color32(name.as_theme());
(
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
)
}
KomorebiTheme::Base16 {
name,
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
..
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::Base16Value::Base0D)
.color32(*name);
let stack_border = stack_border
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::Base16Value::Base0F)
.color32(*name);
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
let floating_border = floating_border
.unwrap_or(komorebi_themes::Base16Value::Base09)
.color32(*name);
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::Base16Value::Base05)
.color32(*name);
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
(
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
)
}
};
border_manager::FOCUSED.store(u32::from(Colour::from(single_border)), Ordering::SeqCst);
border_manager::MONOCLE
.store(u32::from(Colour::from(monocle_border)), Ordering::SeqCst);
border_manager::STACK.store(u32::from(Colour::from(stack_border)), Ordering::SeqCst);
border_manager::FLOATING
.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst);
border_manager::UNFOCUSED
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
STACKBAR_TAB_BACKGROUND_COLOUR.store(
u32::from(Colour::from(stackbar_background)),
Ordering::SeqCst,
);
STACKBAR_FOCUSED_TEXT_COLOUR.store(
u32::from(Colour::from(stackbar_focused_text)),
Ordering::SeqCst,
);
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(
u32::from(Colour::from(stackbar_unfocused_text)),
Ordering::SeqCst,
);
}
if let Some(path) = &self.app_specific_configuration_path {
let path = resolve_home_path(path)?;
let content = std::fs::read_to_string(path)?;
let asc = ApplicationConfigurationGenerator::load(&content)?;
for mut entry in asc {
if let Some(rules) = &mut entry.float_identifiers {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
if let Some(rules) = &mut entry.ignore_identifiers {
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
}
if let Some(ref options) = entry.options {
@@ -829,30 +936,42 @@ impl StaticConfig {
Ok(())
}
pub fn read(path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let value: Self = serde_json::from_str(&content)?;
Ok(value)
}
#[allow(clippy::too_many_lines)]
pub fn preload(
path: &PathBuf,
incoming: Receiver<WindowManagerEvent>,
unix_listener: Option<UnixListener>,
) -> Result<WindowManager> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
value.apply_globals()?;
let socket = DATA_DIR.join("komorebi.sock");
let listener = match unix_listener {
Some(listener) => listener,
None => {
let socket = DATA_DIR.join("komorebi.sock");
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
ErrorKind::NotFound => {}
_ => {
return Err(error.into());
}
},
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
ErrorKind::NotFound => {}
_ => {
return Err(error.into());
}
},
};
UnixListener::bind(&socket)?
}
};
let listener = UnixListener::bind(&socket)?;
let mut wm = WindowManager {
monitors: Ring::default(),
incoming_events: incoming,
@@ -866,6 +985,9 @@ impl StaticConfig {
cross_monitor_move_behaviour: value
.cross_monitor_move_behaviour
.unwrap_or(MoveBehaviour::Swap),
cross_boundary_behaviour: value
.cross_boundary_behaviour
.unwrap_or(CrossBoundaryBehaviour::Monitor),
unmanaged_window_operation_behaviour: value
.unmanaged_window_operation_behaviour
.unwrap_or(OperationBehaviour::Op),
@@ -937,22 +1059,35 @@ impl StaticConfig {
}
}
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
for (j, ws) in monitor.workspaces.iter().enumerate() {
if let Some(rules) = &ws.workspace_rules {
for r in rules {
wm.handle_workspace_rules(&r.id, i, j, false)?;
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: false,
});
}
}
if let Some(rules) = &ws.initial_workspace_rules {
for r in rules {
wm.handle_workspace_rules(&r.id, i, j, true)?;
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: true,
});
}
}
}
}
}
wm.enforce_workspace_rules()?;
if value.border == Some(true) {
border_manager::BORDER_ENABLED.store(true, Ordering::SeqCst);
}
@@ -970,7 +1105,9 @@ impl StaticConfig {
for (i, monitor) in monitors.iter().enumerate() {
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
m.set_work_area_offset(monitor.work_area_offset);
if m.work_area_offset().is_none() {
m.set_work_area_offset(monitor.work_area_offset);
}
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
m.set_window_based_work_area_offset_limit(
monitor.window_based_work_area_offset_limit.unwrap_or(1),
@@ -986,22 +1123,36 @@ impl StaticConfig {
}
}
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_matching_rules.clear();
for (j, ws) in monitor.workspaces.iter().enumerate() {
if let Some(rules) = &ws.workspace_rules {
for r in rules {
wm.handle_workspace_rules(&r.id, i, j, false)?;
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: false,
});
}
}
if let Some(rules) = &ws.initial_workspace_rules {
for r in rules {
wm.handle_workspace_rules(&r.id, i, j, true)?;
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: true,
});
}
}
}
}
}
wm.enforce_workspace_rules()?;
if let Some(enabled) = value.border {
border_manager::BORDER_ENABLED.store(enabled, Ordering::SeqCst);
}
@@ -1014,6 +1165,10 @@ impl StaticConfig {
wm.cross_monitor_move_behaviour = val;
}
if let Some(val) = value.cross_boundary_behaviour {
wm.cross_boundary_behaviour = val;
}
if let Some(val) = value.unmanaged_window_operation_behaviour {
wm.unmanaged_window_operation_behaviour = val;
}

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,10 @@ 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 std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Display;
@@ -15,12 +15,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 +29,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 +39,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 +55,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 +71,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 +146,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 +154,7 @@ impl Serialize for Window {
impl Window {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
@@ -171,28 +172,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 +201,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 +209,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 +220,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 +253,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 +271,18 @@ 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());
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 +294,7 @@ impl Window {
programmatically_hidden_hwnds.remove(idx);
}
WindowsApi::maximize_window(self.hwnd());
WindowsApi::maximize_window(self.hwnd);
}
pub fn unmaximize(self) {
@@ -306,27 +306,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 +337,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 +356,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 +399,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 +449,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 +532,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 +561,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

View File

@@ -16,37 +16,40 @@ 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::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 +66,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 {
@@ -92,6 +94,7 @@ pub struct WindowManager {
pub resize_delta: i32,
pub window_container_behaviour: WindowContainerBehaviour,
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,
@@ -133,14 +136,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,
@@ -182,14 +186,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(),
@@ -231,7 +235,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
@@ -274,6 +277,7 @@ impl WindowManager {
work_area_offset: None,
window_container_behaviour: WindowContainerBehaviour::Create,
cross_monitor_move_behaviour: MoveBehaviour::Swap,
cross_boundary_behaviour: CrossBoundaryBehaviour::Workspace,
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
resize_delta: 50,
focus_follows_mouse: None,
@@ -447,7 +451,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 +462,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,7 +819,7 @@ 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!());
@@ -953,6 +956,7 @@ 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() {
@@ -966,7 +970,9 @@ impl WindowManager {
window.opaque()?;
}
window.remove_accent()?;
if matches!(border_implementation, BorderImplementation::Windows) {
window.remove_accent()?;
}
window.restore();
}
@@ -1109,7 +1115,6 @@ impl WindowManager {
follow: bool,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
ANIMATION_TEMPORARY_DISABLED.store(true, Ordering::SeqCst);
tracing::info!("moving container");
@@ -1117,7 +1122,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);
}
}
@@ -1181,15 +1186,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 +1205,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 +1247,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 +1259,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 +1331,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 +1392,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 +1406,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 +1485,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 +1635,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 +1720,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 +2605,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)>> {
@@ -288,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);
@@ -297,6 +313,10 @@ impl WindowsApi {
}
}
monitors
.elements_mut()
.retain(|m| m.name().ne("PLACEHOLDER"));
Ok(())
}
@@ -324,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);
}
}
}
@@ -343,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
@@ -404,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.
@@ -438,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,
@@ -449,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);
}
@@ -463,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);
}
@@ -490,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()
@@ -502,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,
@@ -511,7 +548,7 @@ impl WindowsApi {
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
)
.process();
SetForegroundWindow(hwnd)
SetForegroundWindow(HWND(as_ptr!(hwnd)))
}
.ok()
.process()
@@ -519,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> {
@@ -527,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>> {
@@ -547,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() {
@@ -567,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))
}
}
@@ -577,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()?;
@@ -593,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,
@@ -606,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<()> {
@@ -632,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)
@@ -661,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,
@@ -673,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
@@ -689,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])?)
@@ -747,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>())?,
@@ -775,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)?;
@@ -785,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> {
@@ -950,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),
@@ -974,7 +1060,7 @@ impl WindowsApi {
unsafe {
DwmSetWindowAttribute(
HWND(hwnd),
HWND(as_ptr!(hwnd)),
DWMWA_WINDOW_CORNER_PREFERENCE,
std::ptr::addr_of!(round).cast(),
4,
@@ -987,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,
@@ -996,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,
@@ -1009,9 +1095,9 @@ impl WindowsApi {
CW_USEDEFAULT,
None,
None,
instance,
HINSTANCE(as_ptr!(instance)),
None,
);
)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
@@ -1020,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,
@@ -1042,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 {
@@ -1105,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;
@@ -221,18 +221,19 @@ impl Workspace {
container.restore();
}
for window in self.floating_windows() {
window.restore();
}
if let Some(container) = self.focused_container_mut() {
container.focus_window(container.focused_window_idx());
}
for window in self.floating_windows() {
window.restore();
}
// 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 +268,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 +358,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 +394,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![];
@@ -655,13 +637,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 +1429,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,30 +11,30 @@ 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"] }

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

@@ -19,7 +19,7 @@
"null"
],
"items": {
"$ref": "#/definitions/IdWithIdentifierAndComment"
"$ref": "#/definitions/MatchingRule"
}
},
"identifier": {
@@ -53,9 +53,9 @@
"enum": [
"object_name_change",
"layered",
"border_overflow",
"tray_and_multi_window",
"force"
"force",
"border_overflow"
]
},
"IdWithIdentifier": {
@@ -83,36 +83,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 +104,11 @@
"StartsWith",
"EndsWith",
"Contains",
"Regex"
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}

1245
schema.bar.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.28`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.30`",
"type": "object",
"properties": {
"animation": {
@@ -236,7 +236,7 @@
}
},
"border_implementation": {
"description": "Display an active window border (default: false)",
"description": "Active window border implementation (default: Komorebi)",
"oneOf": [
{
"description": "Use the adjustable komorebi border implementation",
@@ -383,6 +383,25 @@
"TopMost"
]
},
"cross_boundary_behaviour": {
"description": "Determine what happens when an action is called on a window at a monitor boundary (default: Monitor)",
"oneOf": [
{
"description": "Attempt to perform actions across a workspace boundary",
"type": "string",
"enum": [
"Workspace"
]
},
{
"description": "Attempt to perform actions across a monitor boundary",
"type": "string",
"enum": [
"Monitor"
]
}
]
},
"cross_monitor_move_behaviour": {
"description": "Determine what happens when a window is moved across a monitor boundary (default: Swap)",
"oneOf": [
@@ -1138,7 +1157,7 @@
"format": "int32"
},
"label": {
"description": "Stackbar height",
"description": "Stackbar label",
"type": "string",
"enum": [
"Process",
@@ -1293,6 +1312,760 @@
}
}
},
"theme": {
"description": "Theme configuration options",
"oneOf": [
{
"description": "A theme from catppuccin-egui",
"type": "object",
"required": [
"name",
"palette"
],
"properties": {
"bar_accent": {
"description": "Komorebi status bar accent (default: Blue)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"monocle_border": {
"description": "Border colour when the container is in monocle mode (default: Pink)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"name": {
"description": "Name of the Catppuccin theme",
"type": "string",
"enum": [
"Frappe",
"Latte",
"Macchiato",
"Mocha"
]
},
"palette": {
"type": "string",
"enum": [
"Catppuccin"
]
},
"single_border": {
"description": "Border colour when the container contains a single window (default: Blue)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"stack_border": {
"description": "Border colour when the container contains multiple windows (default: Green)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"stackbar_background": {
"description": "Stackbar tab background colour (default: Base)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"stackbar_focused_text": {
"description": "Stackbar focused tab text colour (default: Green)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"stackbar_unfocused_text": {
"description": "Stackbar unfocused tab text colour (default: Text)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
},
"unfocused_border": {
"description": "Border colour when the container is unfocused (default: Base)",
"type": "string",
"enum": [
"Rosewater",
"Flamingo",
"Pink",
"Mauve",
"Red",
"Maroon",
"Peach",
"Yellow",
"Green",
"Teal",
"Sky",
"Sapphire",
"Blue",
"Lavender",
"Text",
"Subtext1",
"Subtext0",
"Overlay2",
"Overlay1",
"Overlay0",
"Surface2",
"Surface1",
"Surface0",
"Base",
"Mantle",
"Crust"
]
}
}
},
{
"description": "A theme from base16-egui-themes",
"type": "object",
"required": [
"name",
"palette"
],
"properties": {
"bar_accent": {
"description": "Komorebi status bar accent (default: Base0D)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"monocle_border": {
"description": "Border colour when the container is in monocle mode (default: Base0F)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"name": {
"description": "Name of the Base16 theme",
"type": "string",
"enum": [
"3024",
"Apathy",
"Apprentice",
"Ashes",
"AtelierCaveLight",
"AtelierCave",
"AtelierDuneLight",
"AtelierDune",
"AtelierEstuaryLight",
"AtelierEstuary",
"AtelierForestLight",
"AtelierForest",
"AtelierHeathLight",
"AtelierHeath",
"AtelierLakesideLight",
"AtelierLakeside",
"AtelierPlateauLight",
"AtelierPlateau",
"AtelierSavannaLight",
"AtelierSavanna",
"AtelierSeasideLight",
"AtelierSeaside",
"AtelierSulphurpoolLight",
"AtelierSulphurpool",
"Atlas",
"AyuDark",
"AyuLight",
"AyuMirage",
"Aztec",
"Bespin",
"BlackMetalBathory",
"BlackMetalBurzum",
"BlackMetalDarkFuneral",
"BlackMetalGorgoroth",
"BlackMetalImmortal",
"BlackMetalKhold",
"BlackMetalMarduk",
"BlackMetalMayhem",
"BlackMetalNile",
"BlackMetalVenom",
"BlackMetal",
"Blueforest",
"Blueish",
"Brewer",
"Bright",
"Brogrammer",
"BrushtreesDark",
"Brushtrees",
"Caroline",
"CatppuccinFrappe",
"CatppuccinLatte",
"CatppuccinMacchiato",
"CatppuccinMocha",
"Chalk",
"Circus",
"ClassicDark",
"ClassicLight",
"Codeschool",
"Colors",
"Cupcake",
"Cupertino",
"DaOneBlack",
"DaOneGray",
"DaOneOcean",
"DaOnePaper",
"DaOneSea",
"DaOneWhite",
"DanqingLight",
"Danqing",
"Darcula",
"Darkmoss",
"Darktooth",
"Darkviolet",
"Decaf",
"DefaultDark",
"DefaultLight",
"Dirtysea",
"Dracula",
"EdgeDark",
"EdgeLight",
"Eighties",
"EmbersLight",
"Embers",
"Emil",
"EquilibriumDark",
"EquilibriumGrayDark",
"EquilibriumGrayLight",
"EquilibriumLight",
"Eris",
"Espresso",
"EvaDim",
"Eva",
"EvenokDark",
"EverforestDarkHard",
"Everforest",
"Flat",
"Framer",
"FruitSoda",
"Gigavolt",
"Github",
"GoogleDark",
"GoogleLight",
"Gotham",
"GrayscaleDark",
"GrayscaleLight",
"Greenscreen",
"Gruber",
"GruvboxDarkHard",
"GruvboxDarkMedium",
"GruvboxDarkPale",
"GruvboxDarkSoft",
"GruvboxLightHard",
"GruvboxLightMedium",
"GruvboxLightSoft",
"GruvboxMaterialDarkHard",
"GruvboxMaterialDarkMedium",
"GruvboxMaterialDarkSoft",
"GruvboxMaterialLightHard",
"GruvboxMaterialLightMedium",
"GruvboxMaterialLightSoft",
"Hardcore",
"Harmonic16Dark",
"Harmonic16Light",
"HeetchLight",
"Heetch",
"Helios",
"Hopscotch",
"HorizonDark",
"HorizonLight",
"HorizonTerminalDark",
"HorizonTerminalLight",
"HumanoidDark",
"HumanoidLight",
"IaDark",
"IaLight",
"Icy",
"Irblack",
"Isotope",
"Jabuti",
"Kanagawa",
"Katy",
"Kimber",
"Lime",
"Macintosh",
"Marrakesh",
"Materia",
"MaterialDarker",
"MaterialLighter",
"MaterialPalenight",
"MaterialVivid",
"Material",
"MeasuredDark",
"MeasuredLight",
"MellowPurple",
"MexicoLight",
"Mocha",
"Monokai",
"Moonlight",
"Mountain",
"Nebula",
"NordLight",
"Nord",
"Nova",
"Ocean",
"Oceanicnext",
"OneLight",
"OnedarkDark",
"Onedark",
"OutrunDark",
"OxocarbonDark",
"OxocarbonLight",
"Pandora",
"PapercolorDark",
"PapercolorLight",
"Paraiso",
"Pasque",
"Phd",
"Pico",
"Pinky",
"Pop",
"Porple",
"PreciousDarkEleven",
"PreciousDarkFifteen",
"PreciousLightWarm",
"PreciousLightWhite",
"PrimerDarkDimmed",
"PrimerDark",
"PrimerLight",
"Purpledream",
"Qualia",
"Railscasts",
"Rebecca",
"RosePineDawn",
"RosePineMoon",
"RosePine",
"Saga",
"Sagelight",
"Sakura",
"Sandcastle",
"SelenizedBlack",
"SelenizedDark",
"SelenizedLight",
"SelenizedWhite",
"Seti",
"ShadesOfPurple",
"ShadesmearDark",
"ShadesmearLight",
"Shapeshifter",
"SilkDark",
"SilkLight",
"Snazzy",
"SolarflareLight",
"Solarflare",
"SolarizedDark",
"SolarizedLight",
"Spaceduck",
"Spacemacs",
"Sparky",
"StandardizedDark",
"StandardizedLight",
"Stella",
"StillAlive",
"Summercamp",
"SummerfruitDark",
"SummerfruitLight",
"SynthMidnightDark",
"SynthMidnightLight",
"Tango",
"Tarot",
"Tender",
"TerracottaDark",
"Terracotta",
"TokyoCityDark",
"TokyoCityLight",
"TokyoCityTerminalDark",
"TokyoCityTerminalLight",
"TokyoNightDark",
"TokyoNightLight",
"TokyoNightMoon",
"TokyoNightStorm",
"TokyoNightTerminalDark",
"TokyoNightTerminalLight",
"TokyoNightTerminalStorm",
"TokyodarkTerminal",
"Tokyodark",
"TomorrowNightEighties",
"TomorrowNight",
"Tomorrow",
"Tube",
"Twilight",
"UnikittyDark",
"UnikittyLight",
"UnikittyReversible",
"Uwunicorn",
"Vesper",
"Vice",
"Vulcan",
"Windows10Light",
"Windows10",
"Windows95Light",
"Windows95",
"WindowsHighcontrastLight",
"WindowsHighcontrast",
"WindowsNtLight",
"WindowsNt",
"Woodland",
"XcodeDusk",
"Zenbones",
"Zenburn"
]
},
"palette": {
"type": "string",
"enum": [
"Base16"
]
},
"single_border": {
"description": "Border colour when the container contains a single window (default: Base0D)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stack_border": {
"description": "Border colour when the container contains multiple windows (default: Base0B)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stackbar_background": {
"description": "Stackbar tab background colour (default: Base01)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stackbar_focused_text": {
"description": "Stackbar focused tab text colour (default: Base0B)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"stackbar_unfocused_text": {
"description": "Stackbar unfocused tab text colour (default: Base05)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
},
"unfocused_border": {
"description": "Border colour when the container is unfocused (default: Base01)",
"type": "string",
"enum": [
"Base00",
"Base01",
"Base02",
"Base03",
"Base04",
"Base05",
"Base06",
"Base07",
"Base08",
"Base09",
"Base0A",
"Base0B",
"Base0C",
"Base0D",
"Base0E",
"Base0F"
]
}
}
}
]
},
"transparency": {
"description": "Add transparency to unfocused windows (default: false)",
"type": "boolean"
@@ -1303,6 +2076,89 @@
"format": "uint8",
"minimum": 0.0
},
"transparency_ignore_rules": {
"description": "Individual window transparency ignore rules",
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"type": "string",
"enum": [
"Exe",
"Class",
"Title",
"Path"
]
},
"matching_strategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}
},
{
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"type": "string",
"enum": [
"Exe",
"Class",
"Title",
"Path"
]
},
"matching_strategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}
}
}
]
}
},
"tray_and_multi_window_applications": {
"description": "Identify tray and multi-window applications",
"type": "array",

Binary file not shown.

View File

@@ -98,6 +98,9 @@
<Component Id='binary3' Guid='*'>
<File Id='exe3' Name='komorebi-gui.exe' DiskId='1' Source='$(var.CargoTargetBinDir)\komorebi-gui.exe' KeyPath='yes' />
</Component>
<Component Id='binary4' Guid='*'>
<File Id='exe4' Name='komorebi-bar.exe' DiskId='1' Source='$(var.CargoTargetBinDir)\komorebi-bar.exe' KeyPath='yes' />
</Component>
</Directory>
</Directory>
</Directory>
@@ -118,6 +121,8 @@
<ComponentRef Id='binary3' />
<ComponentRef Id='binary4' />
<Feature Id='Environment' Title='PATH Environment Variable' Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.' Level='1' Absent='allow'>
<ComponentRef Id='Path' />
</Feature>