Compare commits

..

30 Commits

Author SHA1 Message Date
LGUG2Z
70a12802db feat(wm): use monitor hardware ids where available
This commit pulls in changes to win32-display-data which provide the
monitor hardware serial number id taken from WmiMonitorID where
available.

No work has yet been done to integrate this with options such as
display_index_preferences.
2025-01-25 15:37:46 -08:00
LGUG2Z
15e443a46b feat(config): add object name change title ignore list
This commit adds a title regex-based ignore list for applications
identified in object_name_change_applications. When a title change on an
EVENT_OBJECT_NAMECHANGE matches one of these regexes, the event will
never be processed as a Show.

This is an edge case workaround specifically targeting the issue of web
apps in Gecko-based browsers which update their page titles at a fixed
regular interval, which was highlighted in #1235.

resolve #1235
2025-01-24 15:50:45 -08:00
LGUG2Z
367ae95cd4 fix(wm): populate ws rules on config reload
This commit fixes a bug where workspace rules would not be populated
properly on file reloads, leading to issues with the
ReplaceConfiguration message handler.
2025-01-24 14:37:43 -08:00
LGUG2Z
edcba65156 fix(bar): consider all window types when hiding empty ws
This commit ensures that floating windows, monocle containers and
maximized windows will be considered when the hide_empty_workspaces
option is enabled for the komorebi widget.

re #1131
2025-01-24 12:48:12 -08:00
alex-ds13
4a8362336f feat(bar): update bar on display connection change
Use the new `MonitorNotification` to reapply the config and recalculate
the position on `MonitorNotification::DisplayConnectionChange`.
2025-01-24 11:48:29 -08:00
alex-ds13
5c3c3659b5 feat(wm): notify subscribers of monitor events
This commit allows notifying the subscribers of any monitor events like
display connection change or work area change.
2025-01-24 11:48:29 -08:00
Csaba
4123c9a0e2 refactor(bar): resolve env vars with pathext
This commit introduces a new PathExt trait with a fn replace_env which
can ensure all environemnt variables are loaded for a PathBuf.

As part of the initial rollout this is used in komorebi-bar to look up
environment variables for the configuration switcher widget.

resolve #1131
2025-01-24 11:44:13 -08:00
Samu-K
cfd89c274c feat(bar): add modifiers for strftime integer formatters
Added the ability of use modifiers with custom format on the Date widget.

For example if using %U returns 04, you can add a modifier so that bar
date widget shows 05.
2025-01-24 10:41:48 -08:00
tieniu
4f041123d1 docs(mkdocs): add note to update asc path
Add note about updating app_specific_configuration_path when using
KOMOREBI_CONFIG_HOME
2025-01-24 10:41:48 -08:00
LGUG2Z
473e7cd6a0 feat(config): add aspect ratios for float toggling
This commit adds a new configuration option
"floating_window_aspect_ratio", which users can manipulate to set their
desired window size when using the toggle-float command.

resolve #1230
2025-01-24 10:41:45 -08:00
LGUG2Z
0a2dbed116 fix(wm): handle hide events for layered windows
This commit ensures that Hide events on Layered windows (usually added
when the transparency feature is enabled) will always be considered
eligible for handling.

This will avoid situations where ghost borders are left behind because
the Hide event was ignored.

fix #878
2025-01-23 16:43:17 -08:00
LGUG2Z
067a279c58 fix(wm): respect mff on cross-monitor monocle focus
This commit ensures that if mouse-follows-focus is disabled, the cursor
will not follow a focus change to a monocle container on an adjacent
monitor.

fix #1119
2025-01-23 16:11:30 -08:00
LGUG2Z
1101baa722 feat(wm): remove min window resize dimensions
This commit removes the minimum window resize dimensions restriction as
most apps now implement these restrictions themselves.

resolve #531
2025-01-23 16:03:24 -08:00
LGUG2Z
da156c091e chore(github): update issue workflows 2025-01-23 15:57:46 -08:00
alex-ds13
39621c14db fix(wm): stop wrongfully removing layout-flip
This commit removes the code on the workspace `update` on `layout-rules`
where it was setting the `layout-flip` to `None` if the layout was
different from `BSP`. This appears to be some old code when the
layout-flip would only apply to the `BSP` layout. However now it appears
to apply to all layouts so this code shouldn't exist. This commit also
changes the docs from the `FlipLayout` command to remove the statement
that only applied to `BSP` since it is no longer true.
2025-01-23 07:46:30 -08:00
dependabot[bot]
e153d2ea0c chore(deps): bump serde_json from 1.0.135 to 1.0.137
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.135 to 1.0.137.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.135...v1.0.137)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 20:37:51 -08:00
dependabot[bot]
e01c3e3c71 chore(deps): bump bitflags from 2.7.0 to 2.8.0
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.7.0...2.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 20:37:33 -08:00
alex-ds13
d7fcbb7d00 fix(bar): pass reconnect event to bar
This commit changes the `rx_gui` from receiving just a notification from
komorebi to now receive a new type `KomorebiEvent` which can be either a
`KomorebiEvent::Notification(komorebi_client::Notification)` or a
`KomorebiEvent::Reconnect`.

The `Reconnect` is sent after losing connection with komorebi and then
reconnecting again.

Now on the bar `update` we check for this `rx_gui` if we get a
notification we pass that to the
`KomorebiNotificationState::handle_notification` function just like
before (except now it takes a notification directly instead of taking
the `rx_gui` and checking for some message on the channel).

If instead we get a `Reconnect` we send a `MonitorWorkAreaOffset` socket
message to komorebi to update the work area offset.
2025-01-22 19:03:38 -08:00
alex-ds13
3e1fc6123a fix(bar): simplify komorebi-bar config
This interactively rebased commit is comprised of the subsequent
individual commits listed further below.

At a high level:

- work_area_offset is now automatically calculated by default
- monitor can now take an index in addition to the previous object
- position can largely be replaced by margin and padding for bars that
  are positioned at the top of the screen
- frame can now largely be replaced by margin and padding for bars that
  are positioned at the top of the screen
- height is now a more intuitive configuration option for setting the
  height of the bar

Detailed explainations and examples are included in the body of PR #1224
on GitHub: https://github.com/LGUG2Z/komorebi/pull/1224

fix(bar): add simplified config for bar

This commit creates a few new config options for the bar that should
make it a lot simpler for new users to configure the bar.

- Remove the need for `position`: if a position is given the bar will
  still use it with priority over the new config. Instead of position
  you can now use the following:
  - `height`: defines the height of the bar (50 by default)
  - `horizontal_margin`: defines the left and right offset of the bar, it
  is the same as setting a `position.start.x` and then remove the same
  amount on `position.end.x`.
  - `vertical_margin`: defines the top and bottom offset of the bar, it is
  the same as setting a `position.start.y` and then add a correct amount
  on the `work_area_offset`.

- Remove the need for `frame`: some new configs were added that take
  priority over the old `frame`. These are:
  - `horizontal_padding`: defines the left and right padding of the bar.
    Similar to `frame.inner_margin.x`.
  - `vertical_padding`: defines the top and bottom padding of the bar.
    Similar to `frame.inner_margin.y`.

- Remove the need for `work_area_offset`: if a `work_area_offset` is
  given then it will take priority, if not, then it will calculate the
  necessary `work_area_offset` using the bar height, position and
  horizontal and vertical margins.

feat(bar): set margin/padding as one or two values

This commit changes the `horizontal_margin`, `vertical_margin`,
`horizontal_padding` and `vertical_padding` to now take a
`SpacingAxisConfig` which can take a single value or two values.

For example, you can set the vertical margin of the bar to add some
spacing above and below like this:

```json
"vertical_margin": 10
```

Which will add a spacing of 10 above and below the bar. Or you can set
it like this:

```json
"vertical_margin": [10, 0]
```

Which will add a spacing of 10 above the bar but no spacing below. You
can even set something like this:

```json
"vertical_margin": [0, -10]
```

To make no spacing above and a negative spacing below to make it so the
tiled windows show right next to the bar. This will basically be
removing the workspace and container padding between the tiled windows
and the bar.

fix(bar): use a right_to_left layout on right side

This commit changes the right area with the right widgets to have a
different layout that is still right_to_left as previously but behaves
much better in regards to its height.

fix(bar): use default bar height

When there is no `work_area_offset` and no `height` on the config it was
using the `BAR_HEIGHT` as default, however the automatica
work_area_offset calculation wasn't being done properly. Now it is!

feat(bar): monitor can be `MonitorConfig` or index

This commit allows the `"monitor":` config to take a `MonitorConfig`
object like it used to or simply a number (index).

docs(schema): update all json schemas

fix(bar): update example bar config

fix(bar): correct work_area_offset on secondary monitors

feat(bar): add multiple options for margin/padding

This commit removes the previous `horizontal_margin`, `vertical_margin`,
`horizontal_padding` and `vertical_padding`, replacing them all with
just `margin` and `padding`.

These new options can be set either with a single value that sets that
spacing on all sides, with an object specifying each individual side or
with an object specifying some "vertical" and/or "horizontal" spacing
which can have a single value, resulting on a symmetric spacing for that
specific axis or two values to define each side of the axis individually.
2025-01-22 18:57:32 -08:00
LGUG2Z
d09d16d291 feat(cli): add focused-workspace-name query
This commit adds the StateQuery::FocusedWorkspaceName variant to allow
users to query the name of the focused workspace via the komorebic query
command.

re #1238
2025-01-22 16:35:39 -08:00
LGUG2Z
77ef259ea8 fix(wm): handle minimize event edge case
This commit handles an edge case where minimize events would not be
processed if both transparency and animations were enabled at the same
time.

fix #1231
2025-01-17 16:06:13 -08:00
LGUG2Z
392e4cc0c9 feat(bar): add cjk font fallbacks
This commit adds CJK font fallbacks to Microsoft YaHei and Malgun
Gothic. This will be looked up at runtime on the user's system, and only
loaded if the files exist in the default Windows font installation
location.

resolve #1139
2025-01-16 16:39:46 -08:00
dependabot[bot]
129dc5d43f chore(deps): bump winreg from 0.53.0 to 0.55.0
Bumps [winreg](https://github.com/gentoo90/winreg-rs) from 0.53.0 to 0.55.0.
- [Release notes](https://github.com/gentoo90/winreg-rs/releases)
- [Changelog](https://github.com/gentoo90/winreg-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gentoo90/winreg-rs/compare/v0.53.0...v0.55.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 14:54:43 -08:00
LGUG2Z
eb6e12e2bd test(wm): add backwards compat integration test 2025-01-11 18:29:10 -08:00
Csaba
a069db611f feat(bar): binary clock and no-second time formats 2025-01-11 15:11:20 -08:00
LGUG2Z
b451df0379 chore(dev): begin v0.1.34-dev 2025-01-11 15:06:50 -08:00
LGUG2Z
cc51f62c3a chore(release): v0.1.33 2025-01-11 13:33:13 -08:00
Csaba
b1db417df5 feat(bar): opt to hide battery widget when charged 2025-01-09 15:49:51 -08:00
LGUG2Z
996a556984 chore(clippy): apply new rust 1.84.0 lints 2025-01-09 15:48:41 -08:00
LGUG2Z
c71e61fb1e chore(deps): cargo update 2025-01-08 21:39:21 -08:00
52 changed files with 2134 additions and 612 deletions

View File

@@ -8,9 +8,9 @@ body:
value: |
Please **do not** open an issue for applications with invisible windows leaving ghost tiles.
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that information to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers , then this is not a bug with komorebi but a bug with the application you are using, and should open an issue with the developer(s) of that application.
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers, then this is not a bug with komorebi but a bug with the application you are using, and you should open an issue with the developer(s) of that application.
- type: textarea
validations:
required: true

View File

@@ -1,21 +1,21 @@
name: Feature request
description: Suggest a new feature (Sponsors only)
description: Suggest a new feature (Limited to Sponsors, Commercial License Holders, and Collaborators)
labels: [enhancement]
title: "[FEAT]: "
body:
- type: dropdown
id: Sponsors
id: Eligibility
attributes:
label: Sponsorship Information
label: Eligibility
description: >
Feature requests are considered from individuals who are $5+ monthly sponsors to the project.
Feature requests are considered from individuals who are current $5+ monthly sponsors to the project, individual commercial use license holders, and approved collaborators.
Please specify the platform you use to sponsor the project.
options:
- GitHub Sponsors
- Ko-fi
- Discord
- YouTube
- Individual Commercial Use License
- GitHub Sponsor
- Ko-fi Sponsor
- Approved Collaborator
default: 0
validations:
required: true

47
.github/workflows/feature-check.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Feature Issue Check
on:
issues:
types: [ opened ]
jobs:
auto-close:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Check and close feature issues
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
if (issue.title.startsWith('[FEAT]: ')) {
const message = `
Feature requests on this repository are only open to current [GitHub sponsors](https://github.com/sponsors/LGUG2Z) on the $5/month tier and above, people with a valid [individual commercial use license](https://lgug2z.com/software/komorebi), and approved contributors.
This issue has been automatically closed until one of those pre-requisites can be validated.
`.replace(/^\s+/gm, '');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: message,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed'
});
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'resolved'
});
}

View File

@@ -1,125 +0,0 @@
name: Feature Request Sponsor Check
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
test_username:
description: "Test username to check sponsorship for"
required: true
default: "octocat"
test_title:
description: "Test issue title"
required: true
default: "[FEAT] Test Feature Request"
test_sponsor_platform:
description: "Selected sponsor platform"
required: true
type: choice
options:
- "GitHub Sponsors"
- "Ko-fi"
- "Discord"
- "YouTube"
jobs:
check-sponsor:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch') || (github.event_name == 'issues' &&
startsWith(github.event.issue.title, '[FEAT]') &&
github.event.issue.user.login != 'LGUG2Z' &&
fromJSON(github.event.issue.body).Sponsors == 'GitHub Sponsors')
steps:
- name: Get Issue Details
id: issue-details
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "username=${{ github.event.inputs.test_username }}" >> $GITHUB_OUTPUT
echo "title=${{ github.event.inputs.test_title }}" >> $GITHUB_OUTPUT
echo "sponsor_platform=${{ github.event.inputs.test_sponsor_platform }}" >> $GITHUB_OUTPUT
else
echo "username=${{ github.event.issue.user.login }}" >> $GITHUB_OUTPUT
echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
echo "sponsor_platform=$(jq -r '.Sponsors' <<< '${{ github.event.issue.body }}')" >> $GITHUB_OUTPUT
fi
- name: Get Sponsorship Status
id: sponsorship
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PAT }}
script: |
const username = '${{ steps.issue-details.outputs.username }}';
const sponsorPlatform = '${{ steps.issue-details.outputs.sponsor_platform }}';
if (sponsorPlatform !== 'GitHub Sponsors') {
console.log('Sponsor platform is not GitHub Sponsors, skipping check');
return true;
}
const sponsorshipQuery = `query($user: String!) {
user(login: $user) {
... on Sponsorable {
sponsorshipForViewerAsSponsorable {
tier {
name
monthlyPriceInDollars
}
}
}
}
}`;
try {
const result = await github.graphql(sponsorshipQuery, {
user: username
});
console.log(result);
const sponsorship = result.user.sponsorshipForViewerAsSponsorable;
console.log(sponsorship);
const amount = sponsorship?.tier?.monthlyPriceInDollars || 0;
console.log(`Sponsorship amount for ${username}: $${amount}/month`);
return amount >= 5;
} catch (error) {
console.log(`Error checking sponsorship: ${error.message}`);
return false;
}
- name: Print Test Results
if: github.event_name == 'workflow_dispatch'
run: |
echo "Test Results for ${{ steps.issue-details.outputs.username }}:"
echo "Title: ${{ steps.issue-details.outputs.title }}"
echo "Platform: ${{ steps.issue-details.outputs.sponsor_platform }}"
echo "Would close issue: ${{ steps.sponsorship.outputs.result == 'false' }}"
- name: Close Issue If Not Sponsor
if: |
github.event_name == 'issues' &&
steps.sponsorship.outputs.result == 'false'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issueNumber = context.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: 'Thank you for your feature request! This repository requires a GitHub sponsorship of at least $5/month to submit feature requests. Please consider becoming a sponsor at https://github.com/sponsors/LGUG2Z'
});
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: 'closed'
});

View File

@@ -47,6 +47,7 @@ jobs:
key: ${{ matrix.platform.target }}
- run: cargo +nightly fmt --check
- run: cargo clippy
- run: cargo test --package komorebi --test compat
- uses: houseabsolute/actions-rust-cross@v1
with:
command: "build"

335
Cargo.lock generated
View File

@@ -181,7 +181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
dependencies = [
"android-properties",
"bitflags 2.6.0",
"bitflags 2.8.0",
"cc",
"cesu8",
"jni",
@@ -630,9 +630,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
dependencies = [
"serde",
]
@@ -757,7 +757,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"log",
"polling",
"rustix",
@@ -787,9 +787,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.7"
version = "1.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a"
dependencies = [
"jobserver",
"libc",
@@ -855,15 +855,16 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "clap"
version = "4.5.24"
version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd"
checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
dependencies = [
"clap_builder",
"clap_derive",
@@ -871,9 +872,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.24"
version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd"
checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
dependencies = [
"anstream",
"anstyle",
@@ -1759,6 +1760,21 @@ dependencies = [
"libc",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -1775,6 +1791,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@@ -1823,6 +1850,7 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -1899,7 +1927,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"libc",
"libgit2-sys",
"log",
@@ -1947,7 +1975,7 @@ version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec69412a0bf07ea7607e638b415447857a808846c2b685a43c8aa18bc6d5e499"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"cfg_aliases 0.2.1",
"cgl",
"core-foundation 0.9.4",
@@ -2013,7 +2041,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"gpu-alloc-types",
]
@@ -2023,7 +2051,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
]
[[package]]
@@ -2032,7 +2060,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"gpu-descriptor-types",
"hashbrown",
]
@@ -2043,7 +2071,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
]
[[package]]
@@ -2438,9 +2466,9 @@ dependencies = [
[[package]]
name = "image-webp"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
dependencies = [
"byteorder-lite",
"quick-error",
@@ -2613,9 +2641,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "komorebi"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"clap",
"color-eyre",
"crossbeam-channel",
@@ -2635,6 +2663,7 @@ dependencies = [
"parking_lot",
"paste",
"regex",
"reqwest",
"schemars",
"serde",
"serde_json_lenient",
@@ -2658,7 +2687,7 @@ dependencies = [
[[package]]
name = "komorebi-bar"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"chrono",
"clap",
@@ -2692,7 +2721,7 @@ dependencies = [
[[package]]
name = "komorebi-client"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"komorebi",
"serde_json_lenient",
@@ -2701,7 +2730,7 @@ dependencies = [
[[package]]
name = "komorebi-gui"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"eframe",
"egui_extras",
@@ -2713,7 +2742,7 @@ dependencies = [
[[package]]
name = "komorebi-themes"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"base16-egui-themes",
"catppuccin-egui",
@@ -2726,7 +2755,7 @@ dependencies = [
[[package]]
name = "komorebic"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"chrono",
"clap",
@@ -2746,7 +2775,7 @@ dependencies = [
"serde_yaml",
"shadow-rs",
"sysinfo",
"thiserror 2.0.10",
"thiserror 2.0.11",
"uds_windows",
"which",
"win32-display-data",
@@ -2755,7 +2784,7 @@ dependencies = [
[[package]]
name = "komorebic-no-console"
version = "0.1.33"
version = "0.1.34"
[[package]]
name = "kqueue"
@@ -2839,7 +2868,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"libc",
"redox_syscall 0.5.8",
]
@@ -2966,7 +2995,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block",
"core-graphics-types",
"foreign-types 0.5.0",
@@ -3087,7 +3116,7 @@ checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f"
dependencies = [
"arrayvec",
"bit-set",
"bitflags 2.6.0",
"bitflags 2.8.0",
"cfg_aliases 0.1.1",
"codespan-reporting",
"hexf-parse",
@@ -3132,7 +3161,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"jni-sys",
"log",
"ndk-sys 0.6.0+11769913",
@@ -3211,7 +3240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e"
dependencies = [
"anyhow",
"bitflags 2.6.0",
"bitflags 2.8.0",
"byteorder",
"libc",
"log",
@@ -3254,7 +3283,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"cfg-if 1.0.0",
"cfg_aliases 0.2.1",
"libc",
@@ -3289,7 +3318,7 @@ version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
@@ -3485,7 +3514,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"libc",
"objc2",
@@ -3501,7 +3530,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"objc2",
"objc2-core-location",
@@ -3525,7 +3554,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"objc2",
"objc2-foundation",
@@ -3567,7 +3596,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"dispatch",
"libc",
@@ -3592,7 +3621,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"objc2",
"objc2-foundation",
@@ -3604,7 +3633,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"objc2",
"objc2-foundation",
@@ -3627,7 +3656,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"objc2",
"objc2-cloud-kit",
@@ -3659,7 +3688,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"objc2",
"objc2-core-location",
@@ -3687,7 +3716,7 @@ version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"cfg-if 1.0.0",
"foreign-types 0.3.2",
"libc",
@@ -3986,9 +4015,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.92"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
@@ -4199,7 +4228,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
]
[[package]]
@@ -4347,7 +4376,7 @@ version = "0.38.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"errno",
"libc",
"linux-raw-sys",
@@ -4356,9 +4385,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.20"
version = "0.23.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [
"once_cell",
"rustls-pki-types",
@@ -4478,7 +4507,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
@@ -4528,9 +4557,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [
"itoa",
"memchr",
@@ -4718,7 +4747,7 @@ version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"calloop",
"calloop-wayland-source",
"cursor-icon",
@@ -4779,7 +4808,7 @@ version = "0.3.0+sdk-1.3.268.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
]
[[package]]
@@ -4875,9 +4904,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.95"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
@@ -4924,7 +4953,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -5012,11 +5041,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.10"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl 2.0.10",
"thiserror-impl 2.0.11",
]
[[package]]
@@ -5032,9 +5061,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.10"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
@@ -5633,7 +5662,7 @@ version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"rustix",
"wayland-backend",
"wayland-scanner",
@@ -5645,7 +5674,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"cursor-icon",
"wayland-backend",
]
@@ -5667,7 +5696,7 @@ version = "0.32.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@@ -5679,7 +5708,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -5692,7 +5721,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -5798,7 +5827,7 @@ checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a"
dependencies = [
"arrayvec",
"bit-vec",
"bitflags 2.6.0",
"bitflags 2.8.0",
"cfg_aliases 0.1.1",
"document-features",
"indexmap",
@@ -5824,7 +5853,7 @@ dependencies = [
"android_system_properties",
"arrayvec",
"ash",
"bitflags 2.6.0",
"bitflags 2.8.0",
"bytemuck",
"cfg_aliases 0.1.1",
"core-graphics-types",
@@ -5861,7 +5890,7 @@ version = "23.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"js-sys",
"web-sys",
]
@@ -5881,11 +5910,13 @@ dependencies = [
[[package]]
name = "win32-display-data"
version = "0.1.0"
source = "git+https://github.com/LGUG2Z/win32-display-data?rev=dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8#dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8"
source = "git+https://github.com/LGUG2Z/win32-display-data?rev=75286e77c068a89d12adcd6404c9c4874a60acf5#75286e77c068a89d12adcd6404c9c4874a60acf5"
dependencies = [
"itertools",
"serde",
"thiserror 1.0.69",
"windows 0.58.0",
"wmi",
]
[[package]]
@@ -5939,6 +5970,16 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
dependencies = [
"windows-core 0.59.0",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -5969,10 +6010,23 @@ dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
dependencies = [
"windows-implement 0.59.0",
"windows-interface 0.59.0",
"windows-result 0.3.0",
"windows-strings 0.3.0",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-icons"
version = "0.1.0"
@@ -6006,6 +6060,17 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-implement"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
@@ -6028,6 +6093,17 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -6035,7 +6111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result 0.2.0",
"windows-strings",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
@@ -6057,6 +6133,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34"
dependencies = [
"windows-targets 0.53.0",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
@@ -6067,6 +6152,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491"
dependencies = [
"windows-targets 0.53.0",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -6142,13 +6236,29 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@@ -6167,6 +6277,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -6185,6 +6301,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -6203,12 +6325,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -6227,6 +6361,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -6245,6 +6385,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -6263,6 +6409,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -6281,6 +6433,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winit"
version = "0.30.8"
@@ -6290,7 +6448,7 @@ dependencies = [
"ahash",
"android-activity",
"atomic-waker",
"bitflags 2.6.0",
"bitflags 2.8.0",
"block2",
"bytemuck",
"calloop",
@@ -6335,9 +6493,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.6.22"
version = "0.6.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
dependencies = [
"memchr",
]
@@ -6353,12 +6511,12 @@ dependencies = [
[[package]]
name = "winreg"
version = "0.53.0"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a47b489f8fc5b949477e89dca4d1617f162c6c53fbcbefde553ab17b342ff9"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if 1.0.0",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6367,6 +6525,21 @@ version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wmi"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a73f536e843f309e9f7f1036561f844c0d07cf735603cb0fc230ae76e6da9aff"
dependencies = [
"chrono",
"futures",
"log",
"serde",
"thiserror 2.0.11",
"windows 0.59.0",
"windows-core 0.59.0",
]
[[package]]
name = "write16"
version = "1.0.0"
@@ -6433,7 +6606,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"dlib",
"log",
"once_cell",

View File

@@ -33,7 +33,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.33"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "75286e77c068a89d12adcd6404c9c4874a60acf5" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }

View File

@@ -389,7 +389,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.32"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.33"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -10,9 +10,9 @@ Options:
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]
[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]
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the style to. If not specified, sets global style

View File

@@ -1,8 +1,8 @@
# toggle-workspace-float-override
```
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes
the opposite of the global value
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace
previously it takes the opposite of the global value
Usage: komorebic.exe toggle-workspace-float-override

View File

@@ -1,7 +1,8 @@
# toggle-workspace-window-container-behaviour
```
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the global value
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the
global value
Usage: komorebic.exe toggle-workspace-window-container-behaviour

View File

@@ -26,5 +26,15 @@ If you already have configuration files that you wish to keep, move them to the
The next time you run `komorebic start`, any files created by or loaded by
_komorebi_ will be placed or expected to exist in this folder.
After setting `$Env:KOMOREBI_CONFIG_HOME`, make sure to update the path in komorebi.json:
```json
{
"app_specific_configuration_path": "$Env:KOMOREBI_CONFIG_HOME/applications.json"
}
```
This ensures that komorebi can locate all configuration files correctly.
[![Watch the tutorial
video](https://img.youtube.com/vi/C_KWUqQ6kko/hqdefault.jpg)](https://www.youtube.com/watch?v=C_KWUqQ6kko)

View File

@@ -1,14 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.32/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {
"left": 0,
"top": 40,
"right": 0,
"bottom": 40
}
},
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.33/schema.bar.json",
"monitor": 0,
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.32/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.33/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.33"
version = "0.1.34"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,5 +1,7 @@
use crate::config::get_individual_spacing;
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::config::MonitorConfigOrIndex;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::komorebi::Komorebi;
@@ -11,12 +13,15 @@ use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::KomorebiEvent;
use crate::BAR_HEIGHT;
use crate::DEFAULT_PADDING;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::Align;
use eframe::egui::Align2;
use eframe::egui::Area;
@@ -34,16 +39,20 @@ use eframe::egui::Margin;
use eframe::egui::Rgba;
use eframe::egui::Style;
use eframe::egui::TextStyle;
use eframe::egui::Vec2;
use eframe::egui::Visuals;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
use komorebi_client::MonitorNotification;
use komorebi_client::NotificationEvent;
use komorebi_client::SocketMessage;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
@@ -51,18 +60,20 @@ use std::sync::Arc;
pub struct Komobar {
pub hwnd: Option<isize>,
pub monitor_index: usize,
pub config: Arc<KomobarConfig>,
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub center_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_gui: Receiver<KomorebiEvent>,
pub rx_config: Receiver<KomobarConfig>,
pub bg_color: Rc<RefCell<Color32>>,
pub bg_color_with_alpha: Rc<RefCell<Color32>>,
pub scale_factor: f32,
pub size_rect: komorebi_client::Rect,
pub work_area_offset: komorebi_client::Rect,
applied_theme_on_first_frame: bool,
}
@@ -198,7 +209,7 @@ impl Komobar {
// Update the `size_rect` so that the bar position can be changed on the EGUI update
// function
self.update_size_rect(config.position.clone());
self.update_size_rect(config);
self.try_apply_theme(config, ctx);
@@ -269,7 +280,7 @@ impl Komobar {
Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
if widget.workspaces.map_or(false, |w| w.enable) {
if widget.workspaces.is_some_and(|w| w.enable) {
previous.borrow_mut().update_from_config(
&widget.komorebi_notification_state.borrow(),
);
@@ -294,23 +305,64 @@ impl Komobar {
self.center_widgets = center_widgets;
self.right_widgets = right_widgets;
if let (Some(prev_rect), Some(new_rect)) = (
&self.config.monitor.work_area_offset,
&config.monitor.work_area_offset,
) {
let (monitor_index, config_work_area_offset) = match &config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
(monitor_config.index, monitor_config.work_area_offset)
}
MonitorConfigOrIndex::Index(idx) => (*idx, None),
};
self.monitor_index = monitor_index;
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset) {
if new_rect != prev_rect {
self.work_area_offset = *new_rect;
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(config.monitor.index, *new_rect),
&SocketMessage::MonitorWorkAreaOffset(self.monitor_index, *new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
config.monitor.index,
self.monitor_index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
config.monitor.index
self.monitor_index
);
}
}
} else if let Some(height) = config.height.or(Some(BAR_HEIGHT)) {
// We only add the `bottom_margin` to the work_area_offset since the top margin is
// already considered on the `size_rect.top`
let bottom_margin = config
.margin
.as_ref()
.map_or(0, |v| v.to_individual(0.0).bottom as i32);
let new_rect = komorebi_client::Rect {
left: 0,
top: (height as i32)
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
+ bottom_margin,
right: 0,
bottom: (height as i32)
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
+ bottom_margin,
};
if new_rect != self.work_area_offset {
self.work_area_offset = new_rect;
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(self.monitor_index, new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
self.monitor_index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
self.monitor_index
);
}
}
@@ -324,8 +376,8 @@ impl Komobar {
}
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
fn update_size_rect(&mut self, position: Option<PositionConfig>) {
let position = position.unwrap_or(PositionConfig {
fn update_size_rect(&mut self, config: &KomobarConfig) {
let position = config.position.clone().unwrap_or(PositionConfig {
start: Some(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
@@ -336,16 +388,26 @@ impl Komobar {
}),
});
let start = position.start.unwrap_or(Position {
let mut start = position.start.unwrap_or(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
});
let end = position.end.unwrap_or(Position {
let mut end = position.end.unwrap_or(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
});
if let Some(height) = config.height {
end.y = height;
}
let margin = get_individual_spacing(0.0, &config.margin);
start.y += margin.top;
start.x += margin.left;
end.x -= margin.left + margin.right;
if end.y == 0.0 {
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
}
@@ -446,12 +508,13 @@ impl Komobar {
pub fn new(
cc: &eframe::CreationContext<'_>,
rx_gui: Receiver<komorebi_client::Notification>,
rx_gui: Receiver<KomorebiEvent>,
rx_config: Receiver<KomobarConfig>,
config: Arc<KomobarConfig>,
) -> Self {
let mut komobar = Self {
hwnd: process_hwnd(),
monitor_index: 0,
config: config.clone(),
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
@@ -464,6 +527,7 @@ impl Komobar {
bg_color_with_alpha: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
size_rect: komorebi_client::Rect::default(),
work_area_offset: komorebi_client::Rect::default(),
applied_theme_on_first_frame: false,
};
@@ -503,6 +567,27 @@ impl Komobar {
let mut fonts = FontDefinitions::default();
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
let mut fallbacks = HashMap::new();
fallbacks.insert("Microsoft YaHei", "C:\\Windows\\Fonts\\msyh.ttc"); // chinese
fallbacks.insert("Malgun Gothic", "C:\\Windows\\Fonts\\malgun.ttf"); // korean
for (name, path) in fallbacks {
if let Ok(bytes) = std::fs::read(path) {
fonts
.font_data
.insert(name.to_owned(), Arc::from(FontData::from_owned(bytes)));
for family in [FontFamily::Proportional, FontFamily::Monospace] {
fonts
.families
.entry(family)
.or_default()
.insert(0, name.to_owned());
}
}
}
let property = FontPropertyBuilder::new().family(name).build();
if let Some((font, _)) = system_fonts::get(&property) {
@@ -510,21 +595,17 @@ impl Komobar {
.font_data
.insert(name.to_owned(), Arc::new(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);
for family in [FontFamily::Proportional, FontFamily::Monospace] {
fonts
.families
.entry(family)
.or_default()
.insert(0, name.to_owned());
}
}
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
}
impl eframe::App for Komobar {
@@ -555,20 +636,86 @@ impl eframe::App for Komobar {
);
}
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(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme,
self.render_config.clone(),
);
match self.rx_gui.try_recv() {
Err(error) => match error {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
tracing::error!(
"failed to receive komorebi notification on gui thread: {error}"
);
}
},
Ok(KomorebiEvent::Notification(notification)) => {
let should_apply_config = if matches!(
notification.event,
NotificationEvent::Monitor(MonitorNotification::DisplayConnectionChange)
) {
let state = &notification.state;
// Store the monitor coordinates in case they've changed
MONITOR_RIGHT.store(
state.monitors.elements()[self.monitor_index].size().right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[self.monitor_index].size().top,
Ordering::SeqCst,
);
MONITOR_LEFT.store(
state.monitors.elements()[self.monitor_index].size().left,
Ordering::SeqCst,
);
true
} else {
false
};
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(
ctx,
self.monitor_index,
notification,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme,
self.render_config.clone(),
);
}
if should_apply_config {
self.apply_config(
ctx,
&self.config.clone(),
self.komorebi_notification_state.clone(),
);
}
}
Ok(KomorebiEvent::Reconnect) => {
if let Err(error) =
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
self.monitor_index,
self.work_area_offset,
))
{
tracing::error!(
"error applying work area offset to monitor '{}': {}",
self.monitor_index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
self.monitor_index
);
}
}
}
if !self.applied_theme_on_first_frame {
@@ -601,40 +748,78 @@ impl eframe::App for Komobar {
}
}
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_with_alpha.borrow())
} else {
Frame::none().fill(*self.bg_color_with_alpha.borrow())
let frame = match &self.config.padding {
None => {
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_with_alpha.borrow())
} else {
Frame::none()
.inner_margin(Margin::same(0.0))
.fill(*self.bg_color_with_alpha.borrow())
}
}
Some(padding) => {
let padding = padding.to_individual(DEFAULT_PADDING);
Frame::none()
.inner_margin(Margin {
top: padding.top,
bottom: padding.bottom,
left: padding.left,
right: padding.right,
})
.fill(*self.bg_color_with_alpha.borrow())
}
};
let mut render_config = self.render_config.borrow_mut();
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
CentralPanel::default().frame(frame).show(ctx, |_| {
CentralPanel::default().frame(frame).show(ctx, |ui| {
// Apply grouping logic for the bar as a whole
let area_frame = if let Some(frame) = &self.config.frame {
Frame::none().inner_margin(Margin::symmetric(0.0, frame.inner_margin.y))
Frame::none()
.inner_margin(Margin::symmetric(0.0, frame.inner_margin.y))
.outer_margin(Margin::same(0.0))
} else {
Frame::none()
.inner_margin(Margin::same(0.0))
.outer_margin(Margin::same(0.0))
};
let available_height = ui.max_rect().max.y;
ctx.style_mut(|style| {
style.spacing.interact_size.y = available_height;
});
if !self.left_widgets.is_empty() {
// Left-aligned widgets layout
Area::new(Id::new("left_panel"))
.anchor(Align2::LEFT_CENTER, [0.0, 0.0]) // Align in the left center of the window
.show(ctx, |ui| {
let mut left_area_frame = area_frame;
if let Some(frame) = &self.config.frame {
if let Some(padding) = self
.config
.padding
.as_ref()
.map(|s| s.to_individual(DEFAULT_PADDING))
{
left_area_frame.inner_margin.left = padding.left;
left_area_frame.inner_margin.top = padding.top;
left_area_frame.inner_margin.bottom = padding.bottom;
} else if let Some(frame) = &self.config.frame {
left_area_frame.inner_margin.left = frame.inner_margin.x;
left_area_frame.inner_margin.top = frame.inner_margin.y;
left_area_frame.inner_margin.bottom = frame.inner_margin.y;
}
left_area_frame.show(ui, |ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
ui.horizontal(|ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Left);
@@ -654,20 +839,40 @@ impl eframe::App for Komobar {
.anchor(Align2::RIGHT_CENTER, [0.0, 0.0]) // Align in the right center of the window
.show(ctx, |ui| {
let mut right_area_frame = area_frame;
if let Some(frame) = &self.config.frame {
if let Some(padding) = self
.config
.padding
.as_ref()
.map(|s| s.to_individual(DEFAULT_PADDING))
{
right_area_frame.inner_margin.right = padding.right;
right_area_frame.inner_margin.top = padding.top;
right_area_frame.inner_margin.bottom = padding.bottom;
} else if let Some(frame) = &self.config.frame {
right_area_frame.inner_margin.right = frame.inner_margin.x;
right_area_frame.inner_margin.top = frame.inner_margin.y;
right_area_frame.inner_margin.bottom = frame.inner_margin.y;
}
right_area_frame.show(ui, |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Right);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
right_area_frame.show(ui, |ui| {
let initial_size = Vec2 {
x: ui.available_size_before_wrap().x,
y: ui.spacing().interact_size.y,
};
ui.allocate_ui_with_layout(
initial_size,
Layout::right_to_left(Align::Center),
|ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Right);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
},
);
});
});
}
@@ -677,9 +882,22 @@ impl eframe::App for Komobar {
Area::new(Id::new("center_panel"))
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
.show(ctx, |ui| {
let center_area_frame = area_frame;
let mut center_area_frame = area_frame;
if let Some(padding) = self
.config
.padding
.as_ref()
.map(|s| s.to_individual(DEFAULT_PADDING))
{
center_area_frame.inner_margin.top = padding.top;
center_area_frame.inner_margin.bottom = padding.bottom;
} else if let Some(frame) = &self.config.frame {
center_area_frame.inner_margin.top = frame.inner_margin.y;
center_area_frame.inner_margin.bottom = frame.inner_margin.y;
}
center_area_frame.show(ui, |ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
ui.horizontal(|ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Center);

View File

@@ -1,11 +1,11 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
@@ -14,6 +14,7 @@ use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager;
use starship_battery::State;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
@@ -21,6 +22,8 @@ use std::time::Instant;
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
/// Hide the widget if the battery is at full charge
pub hide_on_full_charge: Option<bool>,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
@@ -33,6 +36,7 @@ impl From<BatteryConfig> for Battery {
Self {
enable: value.enable,
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
manager: Manager::new().unwrap(),
last_state: String::new(),
data_refresh_interval,
@@ -52,6 +56,7 @@ pub enum BatteryState {
pub struct Battery {
pub enable: bool,
hide_on_full_charge: bool,
manager: Manager,
pub state: BatteryState,
data_refresh_interval: u64,
@@ -71,17 +76,22 @@ impl Battery {
if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => self.state = BatteryState::Discharging,
_ => {}
}
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
if percentage == 100.0 && self.hide_on_full_charge {
output = String::new()
} else {
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => self.state = BatteryState::Discharging,
_ => {}
}
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
@@ -125,12 +135,18 @@ impl BarWidget for Battery {
},
);
config.apply_on_widget(true, ui, |ui| {
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("cmd.exe")
.args(["/C", "start", "ms-settings:batterysaver"])
.spawn()
{
eprintln!("{}", error)
}
}
});
}
}

View File

@@ -1,5 +1,6 @@
use crate::render::Grouping;
use crate::widget::WidgetConfig;
use crate::DEFAULT_PADDING;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
@@ -12,15 +13,67 @@ use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.33`
/// The `komorebi.bar.json` configuration file reference for `v0.1.34`
pub struct KomobarConfig {
/// Bar height (default: 50)
pub height: Option<f32>,
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
/// vertical definition which can each take a single value for a symmetric padding or two
/// values for each side, i.e.:
/// ```json
/// "padding": {
/// "horizontal": 10
/// }
/// ```
/// or:
/// ```json
/// "padding": {
/// "horizontal": [left, right]
/// }
/// ```
/// You can also set individual padding on each side like this:
/// ```json
/// "padding": {
/// "top": 10,
/// "bottom": 10,
/// "left": 10,
/// "right": 10,
/// }
/// ```
/// By default, padding is set to 10 on all sides.
pub padding: Option<Padding>,
/// Bar margin. Use one value for all sides or use a grouped margin for horizontal and/or
/// vertical definition which can each take a single value for a symmetric margin or two
/// values for each side, i.e.:
/// ```json
/// "margin": {
/// "horizontal": 10
/// }
/// ```
/// or:
/// ```json
/// "margin": {
/// "vertical": [top, bottom]
/// }
/// ```
/// You can also set individual margin on each side like this:
/// ```json
/// "margin": {
/// "top": 10,
/// "bottom": 10,
/// "left": 10,
/// "right": 10,
/// }
/// ```
/// By default, margin is set to 0 on all sides.
pub margin: Option<Margin>,
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// Monitor options
pub monitor: MonitorConfig,
/// The monitor index or the full monitor options
pub monitor: MonitorConfigOrIndex,
/// Font family
pub font_family: Option<String>,
/// Font size (default: 12.5)
@@ -90,6 +143,15 @@ pub struct FrameConfig {
pub inner_margin: Position,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MonitorConfigOrIndex {
/// The monitor index where you want the bar to show
Index(usize),
/// The full monitor options with the index and an optional work_area_offset
MonitorConfig(MonitorConfig),
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
@@ -98,6 +160,154 @@ pub struct MonitorConfig {
pub work_area_offset: Option<Rect>,
}
pub type Padding = SpacingKind;
pub type Margin = SpacingKind;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
// WARNING: To any developer messing with this code in the future: The order here matters!
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
// individual values.
pub enum SpacingKind {
All(f32),
Individual(IndividualSpacingConfig),
Grouped(GroupedSpacingConfig),
}
impl SpacingKind {
pub fn to_individual(&self, default: f32) -> IndividualSpacingConfig {
match self {
SpacingKind::All(m) => IndividualSpacingConfig::all(*m),
SpacingKind::Grouped(grouped_spacing_config) => {
let vm = grouped_spacing_config.vertical.as_ref().map_or(
IndividualSpacingConfig::vertical(default),
|vm| match vm {
GroupedSpacingOptions::Symmetrical(m) => {
IndividualSpacingConfig::vertical(*m)
}
GroupedSpacingOptions::Split(tm, bm) => {
IndividualSpacingConfig::vertical(*tm).bottom(*bm)
}
},
);
let hm = grouped_spacing_config.horizontal.as_ref().map_or(
IndividualSpacingConfig::horizontal(default),
|hm| match hm {
GroupedSpacingOptions::Symmetrical(m) => {
IndividualSpacingConfig::horizontal(*m)
}
GroupedSpacingOptions::Split(lm, rm) => {
IndividualSpacingConfig::horizontal(*lm).right(*rm)
}
},
);
IndividualSpacingConfig {
top: vm.top,
bottom: vm.bottom,
left: hm.left,
right: hm.right,
}
}
SpacingKind::Individual(m) => *m,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct GroupedSpacingConfig {
pub vertical: Option<GroupedSpacingOptions>,
pub horizontal: Option<GroupedSpacingOptions>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum GroupedSpacingOptions {
Symmetrical(f32),
Split(f32, f32),
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct IndividualSpacingConfig {
pub top: f32,
pub bottom: f32,
pub left: f32,
pub right: f32,
}
#[allow(dead_code)]
impl IndividualSpacingConfig {
pub const ZERO: Self = IndividualSpacingConfig {
top: 0.0,
bottom: 0.0,
left: 0.0,
right: 0.0,
};
pub fn all(value: f32) -> Self {
IndividualSpacingConfig {
top: value,
bottom: value,
left: value,
right: value,
}
}
pub fn horizontal(value: f32) -> Self {
IndividualSpacingConfig {
top: 0.0,
bottom: 0.0,
left: value,
right: value,
}
}
pub fn vertical(value: f32) -> Self {
IndividualSpacingConfig {
top: value,
bottom: value,
left: 0.0,
right: 0.0,
}
}
pub fn top(self, value: f32) -> Self {
IndividualSpacingConfig { top: value, ..self }
}
pub fn bottom(self, value: f32) -> Self {
IndividualSpacingConfig {
bottom: value,
..self
}
}
pub fn left(self, value: f32) -> Self {
IndividualSpacingConfig {
left: value,
..self
}
}
pub fn right(self, value: f32) -> Self {
IndividualSpacingConfig {
right: value,
..self
}
}
}
pub fn get_individual_spacing(
default: f32,
spacing: &Option<SpacingKind>,
) -> IndividualSpacingConfig {
spacing
.as_ref()
.map_or(IndividualSpacingConfig::all(default), |s| {
s.to_individual(default)
})
}
impl KomobarConfig {
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
let content = std::fs::read_to_string(path)?;
@@ -108,7 +318,10 @@ impl KomobarConfig {
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position { x: 10.0, y: 10.0 },
inner_margin: Position {
x: DEFAULT_PADDING,
y: DEFAULT_PADDING,
},
});
}

View File

@@ -13,6 +13,48 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
/// Custom format with additive modifiers for integer format specifiers
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CustomModifiers {
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
format: String,
/// Additive modifiers for integer format specifiers (e.g. { "%U": 1 } to increment the zero-indexed week number by 1)
modifiers: std::collections::HashMap<String, i32>,
}
impl CustomModifiers {
fn apply(&self, output: &str) -> String {
let int_formatters = vec![
"%Y", "%C", "%y", "%m", "%d", "%e", "%w", "%u", "%U", "%W", "%G", "%g", "%V", "%j",
"%H", "%k", "%I", "%l", "%M", "%S", "%f",
];
let mut modified_output = output.to_string();
for (modifier, value) in &self.modifiers {
// check if formatter is integer type
if !int_formatters.contains(&modifier.as_str()) {
continue;
}
// get the strftime value of modifier
let formatted_modifier = chrono::Local::now().format(modifier).to_string();
// find the gotten value in the original output
if let Some(pos) = modified_output.find(&formatted_modifier) {
let start = pos;
let end = start + formatted_modifier.len();
// replace that value with the modified value
if let Ok(num) = formatted_modifier.parse::<i32>() {
modified_output.replace_range(start..end, &(num + value).to_string());
}
}
}
modified_output
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DateConfig {
/// Enable the Date widget
@@ -45,6 +87,8 @@ pub enum DateFormat {
DayDateMonthYear,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
/// Custom format with modifiers
CustomModifiers(CustomModifiers),
}
impl DateFormat {
@@ -58,13 +102,14 @@ impl DateFormat {
};
}
fn fmt_string(&self) -> String {
pub 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(),
DateFormat::CustomModifiers(custom) => custom.format.clone(),
}
}
}
@@ -78,9 +123,15 @@ pub struct Date {
impl Date {
fn output(&mut self) -> String {
chrono::Local::now()
let formatted = chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
.to_string();
// if custom modifiers are used, apply them
match &self.format {
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
_ => formatted,
}
}
}

View File

@@ -10,8 +10,6 @@ use crate::widget::BarWidget;
use crate::ICON_CACHE;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_INDEX;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::vec2;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
@@ -30,6 +28,7 @@ use eframe::egui::Vec2;
use image::RgbaImage;
use komorebi_client::Container;
use komorebi_client::NotificationEvent;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_client::Window;
@@ -99,7 +98,7 @@ impl From<&KomorebiConfig> for Komorebi {
if let Some(configuration_switcher) = &value.configuration_switcher {
let mut configuration_switcher = configuration_switcher.clone();
for (_, location) in configuration_switcher.configurations.iter_mut() {
*location = dunce::simplified(&PathBuf::from(location.clone()))
*location = dunce::simplified(&PathBuf::from(location.clone()).replace_env())
.to_string_lossy()
.to_string();
}
@@ -496,7 +495,7 @@ impl KomorebiNotificationState {
&mut self,
ctx: &Context,
monitor_index: usize,
rx_gui: Receiver<komorebi_client::Notification>,
notification: komorebi_client::Notification,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
transparency_alpha: Option<u8>,
@@ -504,119 +503,109 @@ impl KomorebiNotificationState {
default_theme: Option<KomobarTheme>,
render_config: Rc<RefCell<RenderConfig>>,
) {
match rx_gui.try_recv() {
Err(error) => match error {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
tracing::error!(
"failed to receive komorebi notification on gui thread: {error}"
);
}
},
Ok(notification) => {
match notification.event {
NotificationEvent::WindowManager(_) => {}
NotificationEvent::Socket(message) => match message {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from updated komorebi.json");
} else if let Some(default_theme) = default_theme {
apply_theme(
ctx,
default_theme,
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("removed theme from updated komorebi.json and applied default theme");
} else {
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
}
}
}
SocketMessage::Theme(theme) => {
match notification.event {
NotificationEvent::WindowManager(_) => {}
NotificationEvent::Monitor(_) => {}
NotificationEvent::Socket(message) => match message {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color,
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from komorebi socket message");
tracing::info!("applied theme from updated komorebi.json");
} else if let Some(default_theme) = default_theme {
apply_theme(
ctx,
default_theme,
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("removed theme from updated komorebi.json and applied default theme");
} else {
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
}
_ => {}
},
}
self.monitor_index = monitor_index;
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_show = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
} else {
true
};
if should_show {
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
ws.into(),
));
}
}
self.workspaces = workspaces;
if monitor.workspaces()[focused_workspace_idx]
.monocle_container()
.is_some()
{
self.layout = KomorebiLayout::Monocle;
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
} else if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
} else {
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => {
KomorebiLayout::Default(*layout)
}
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
SocketMessage::Theme(theme) => {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color,
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
},
}
self.focused_container_information =
(&monitor.workspaces()[focused_workspace_idx]).into();
self.monitor_index = monitor_index;
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_show = if self.hide_empty_workspaces {
focused_workspace_idx == i
|| !ws.containers().is_empty()
|| !ws.floating_windows().is_empty()
|| ws.monocle_container().is_some()
|| ws.maximized_window().is_some()
} else {
true
};
if should_show {
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
ws.into(),
));
}
}
self.workspaces = workspaces;
if monitor.workspaces()[focused_workspace_idx]
.monocle_container()
.is_some()
{
self.layout = KomorebiLayout::Monocle;
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
} else if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
} else {
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
}
self.focused_container_information = (&monitor.workspaces()[focused_workspace_idx]).into();
}
}

View File

@@ -21,6 +21,7 @@ use crate::config::KomobarConfig;
use crate::config::Position;
use crate::config::PositionConfig;
use clap::Parser;
use config::MonitorConfigOrIndex;
use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
@@ -57,6 +58,7 @@ pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
pub static DEFAULT_PADDING: f32 = 10.0;
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
@@ -113,6 +115,11 @@ fn process_hwnd() -> Option<isize> {
}
}
pub enum KomorebiEvent {
Notification(komorebi_client::Notification),
Reconnect,
}
fn main() -> color_eyre::Result<()> {
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
@@ -230,32 +237,39 @@ fn main() -> color_eyre::Result<()> {
&SocketMessage::State,
)?)?;
let (monitor_index, work_area_offset) = match &config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
(monitor_config.index, monitor_config.work_area_offset)
}
MonitorConfigOrIndex::Index(idx) => (*idx, None),
};
MONITOR_RIGHT.store(
state.monitors.elements()[config.monitor.index].size().right,
state.monitors.elements()[monitor_index].size().right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().top,
state.monitors.elements()[monitor_index].size().top,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().left,
MONITOR_LEFT.store(
state.monitors.elements()[monitor_index].size().left,
Ordering::SeqCst,
);
MONITOR_INDEX.store(config.monitor.index, Ordering::SeqCst);
MONITOR_INDEX.store(monitor_index, Ordering::SeqCst);
match config.position {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
x: state.monitors.elements()[monitor_index].size().right as f32,
y: 50.0,
}),
})
@@ -263,14 +277,14 @@ fn main() -> color_eyre::Result<()> {
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
});
}
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
x: state.monitors.elements()[monitor_index].size().right as f32,
y: 50.0,
})
}
@@ -287,15 +301,9 @@ fn main() -> color_eyre::Result<()> {
..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
);
if let Some(rect) = &work_area_offset {
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(monitor_index, *rect))?;
tracing::info!("work area offset applied to monitor: {}", monitor_index);
}
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
@@ -330,8 +338,6 @@ fn main() -> color_eyre::Result<()> {
"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));
@@ -374,18 +380,12 @@ fn main() -> color_eyre::Result<()> {
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));
}
if let Err(error) = tx_gui.send(KomorebiEvent::Reconnect) {
tracing::error!("could not send komorebi reconnect event to gui thread: {error}")
}
ctx_komorebi.request_repaint();
continue;
}
match String::from_utf8(buffer) {
@@ -396,7 +396,7 @@ fn main() -> color_eyre::Result<()> {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(notification) {
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}

View File

@@ -1,5 +1,6 @@
use crate::bar::Alignment;
use crate::config::KomobarConfig;
use crate::config::MonitorConfigOrIndex;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -81,8 +82,13 @@ impl RenderExt for &KomobarConfig {
let mut icon_font_id = text_font_id.clone();
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
let monitor_idx = match &self.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
MonitorConfigOrIndex::Index(idx) => *idx,
};
RenderConfig {
monitor_idx: self.monitor.index,
monitor_idx,
spacing: self.widget_spacing.unwrap_or(10.0),
grouping: self.grouping.unwrap_or(Grouping::None),
background_color,

View File

@@ -1,3 +1,4 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
@@ -6,8 +7,12 @@ use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -36,8 +41,16 @@ impl From<TimeConfig> for Time {
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
/// Twelve-hour format (without seconds)
TwelveHourWithoutSeconds,
/// Twenty-four-hour format (with seconds)
TwentyFourHour,
/// Twenty-four-hour format (without seconds)
TwentyFourHourWithoutSeconds,
/// Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
BinaryCircle,
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
BinaryRectangle,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
@@ -45,8 +58,12 @@ pub enum TimeFormat {
impl TimeFormat {
pub fn toggle(&mut self) {
match self {
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
TimeFormat::TwelveHour => *self = TimeFormat::TwelveHourWithoutSeconds,
TimeFormat::TwelveHourWithoutSeconds => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwentyFourHourWithoutSeconds,
TimeFormat::TwentyFourHourWithoutSeconds => *self = TimeFormat::BinaryCircle,
TimeFormat::BinaryCircle => *self = TimeFormat::BinaryRectangle,
TimeFormat::BinaryRectangle => *self = TimeFormat::TwelveHour,
_ => {}
};
}
@@ -54,7 +71,11 @@ impl TimeFormat {
fn fmt_string(&self) -> String {
match self {
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
TimeFormat::TwelveHourWithoutSeconds => String::from("%l:%M %p"),
TimeFormat::TwentyFourHour => String::from("%T"),
TimeFormat::TwentyFourHourWithoutSeconds => String::from("%H:%M"),
TimeFormat::BinaryCircle => String::from("c%T"),
TimeFormat::BinaryRectangle => String::from("r%T"),
TimeFormat::Custom(format) => format.to_string(),
}
}
@@ -72,6 +93,169 @@ impl Time {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string()
}
fn paint_binary_circle(
&mut self,
size: f32,
number: u32,
max_power: usize,
ctx: &Context,
ui: &mut Ui,
) {
let full_height = size;
let height = full_height / 4.0;
let width = height;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
let color = ctx.style().visuals.text_color();
let c = response.rect.center();
let r = height / 2.0 - 0.5;
if number == 1 || number == 3 || number == 5 || number == 7 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r / 2.5, color);
}
if number == 2 || number == 3 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r / 2.5, color);
}
if number == 4 || number == 5 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r, color);
} else if max_power > 2 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r / 2.5, color);
}
if number == 8 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r, color);
} else if max_power > 3 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r / 2.5, color);
}
}
fn paint_binary_rect(
&mut self,
size: f32,
number: u32,
max_power: usize,
ctx: &Context,
ui: &mut Ui,
) {
let full_height = size;
let height = full_height / 4.0;
let width = height * 1.5;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
let color = ctx.style().visuals.text_color();
let stroke = Stroke::new(1.0, color);
let round_all = Rounding::same(response.rect.width() * 0.1);
let round_top = Rounding {
nw: round_all.nw,
ne: round_all.ne,
..Default::default()
};
let round_none = Rounding::ZERO;
let round_bottom = Rounding {
sw: round_all.nw,
se: round_all.ne,
..Default::default()
};
if max_power == 2 {
let mut rect = response.rect.shrink(stroke.width);
rect.set_height(rect.height() - height * 2.0);
rect = rect.translate(Vec2::new(0.0, height * 2.0));
painter.rect_stroke(rect, round_all, stroke);
} else if max_power == 3 {
let mut rect = response.rect.shrink(stroke.width);
rect.set_height(rect.height() - height);
rect = rect.translate(Vec2::new(0.0, height));
painter.rect_stroke(rect, round_all, stroke);
} else {
painter.rect_stroke(response.rect.shrink(stroke.width), round_all, stroke);
}
let mut rect_bin = response.rect;
rect_bin.set_width(width);
if number == 1 || number == 5 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 3.0)),
round_bottom,
color,
);
}
if number == 2 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
if max_power == 2 {
round_top
} else {
round_none
},
color,
);
}
if number == 3 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
round_bottom,
color,
);
}
if number == 4 || number == 5 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
if max_power == 3 {
round_top
} else {
round_none
},
color,
);
}
if number == 6 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
if max_power == 3 {
round_top
} else {
round_none
},
color,
);
}
if number == 7 {
rect_bin.set_height(height * 3.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height)),
if max_power == 3 {
round_all
} else {
round_bottom
},
color,
);
}
if number == 8 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(rect_bin.translate(Vec2::new(0.0, 0.0)), round_top, color);
}
}
}
@@ -80,6 +264,9 @@ impl BarWidget for Time {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let use_binary_circle = output.starts_with('c');
let use_binary_rectangle = output.starts_with('r');
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -96,20 +283,83 @@ impl BarWidget for Time {
output.insert_str(0, "TIME: ");
}
layout_job.append(
&output,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
if !use_binary_circle && !use_binary_rectangle {
layout_job.append(
&output,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
}
let font_id = config.icon_font_id.clone();
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.show(ui, |ui| {
if !is_reversed {
ui.add(Label::new(layout_job.clone()).selectable(false));
}
if use_binary_circle || use_binary_rectangle {
let ordered_output = if is_reversed {
output.chars().rev().collect()
} else {
output
};
for (section_index, section) in
ordered_output.split(':').enumerate()
{
ui.scope(|ui| {
ui.spacing_mut().item_spacing = Vec2::splat(2.0);
for (number_index, number_char) in
section.chars().enumerate()
{
if let Some(number) = number_char.to_digit(10) {
// the hour is the second char in the first section (in reverse, it's in the last section)
let max_power = match (
is_reversed,
section_index,
number_index,
) {
(true, 2, 1) | (false, 0, 1) => 2,
(true, _, 1) | (false, _, 0) => 3,
_ => 4,
};
if use_binary_circle {
self.paint_binary_circle(
font_id.size,
number,
max_power,
ctx,
ui,
);
} else if use_binary_rectangle {
self.paint_binary_rect(
font_id.size,
number,
max_power,
ctx,
ui,
);
}
}
}
});
}
}
if is_reversed {
ui.add(Label::new(layout_job.clone()).selectable(false));
}
})
.clicked()
{
self.format.toggle()

View File

@@ -65,13 +65,13 @@ impl WidgetConfig {
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().map_or(false, |w| w.enable)
|| config.layout.as_ref().map_or(false, |w| w.enable)
|| config.focused_window.as_ref().map_or(false, |w| w.enable)
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_window.as_ref().is_some_and(|w| w.enable)
|| config
.configuration_switcher
.as_ref()
.map_or(false, |w| w.enable)
.is_some_and(|w| w.enable)
}
WidgetConfig::Media(config) => config.enable,
WidgetConfig::Memory(config) => config.enable,

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.33"
version = "0.1.34"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -25,6 +25,7 @@ pub use komorebi::core::Layout;
pub use komorebi::core::MoveBehaviour;
pub use komorebi::core::OperationBehaviour;
pub use komorebi::core::OperationDirection;
pub use komorebi::core::PathExt;
pub use komorebi::core::Rect;
pub use komorebi::core::Sizing;
pub use komorebi::core::SocketMessage;
@@ -33,6 +34,7 @@ pub use komorebi::core::StackbarMode;
pub use komorebi::core::StateQuery;
pub use komorebi::core::WindowKind;
pub use komorebi::monitor::Monitor;
pub use komorebi::monitor_reconciliator::MonitorNotification;
pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-gui"
version = "0.1.33"
version = "0.1.34"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-themes"
version = "0.1.33"
version = "0.1.34"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.33"
version = "0.1.34"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
@@ -47,10 +47,13 @@ windows-core = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.53"
winreg = "0.55"
[build-dependencies]
shadow-rs = { workspace = true }
[dev-dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
[features]
deadlock_detection = ["parking_lot/deadlock_detection"]

View File

@@ -15,7 +15,7 @@ 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::Win32::System::Com::COINIT_MULTITHREADED;
use windows_core::Interface;
struct ComInit();
@@ -23,10 +23,7 @@ struct ComInit();
impl ComInit {
pub fn new() -> Self {
unsafe {
// Notice: Only COINIT_APARTMENTTHREADED works correctly!
//
// Not COINIT_MULTITHREADED or CoIncrementMTAUsage, they cause a seldom crashes in threading tests.
CoInitializeEx(None, COINIT_APARTMENTTHREADED).unwrap();
CoInitializeEx(None, COINIT_MULTITHREADED).unwrap();
}
Self()
}

View File

@@ -89,7 +89,6 @@ impl DefaultLayout {
return None;
};
let max_divisor = 1.005;
let mut r = resize.unwrap_or_default();
let resize_delta = delta;
@@ -108,15 +107,13 @@ impl DefaultLayout {
// against this; if people end up in this situation they are better off
// just hitting the retile command
let diff = ((r.left + -resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
if diff < unaltered.right as f32 {
r.left += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.left - -resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
if diff < unaltered.right as f32 {
r.left -= -resize_delta;
}
}
@@ -124,15 +121,13 @@ impl DefaultLayout {
OperationDirection::Up => match sizing {
Sizing::Increase => {
let diff = ((r.top + resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
if diff < unaltered.bottom as f32 {
r.top += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.top - resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
if diff < unaltered.bottom as f32 {
r.top -= -resize_delta;
}
}
@@ -140,15 +135,13 @@ impl DefaultLayout {
OperationDirection::Right => match sizing {
Sizing::Increase => {
let diff = ((r.right + resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
if diff < unaltered.right as f32 {
r.right += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.right - resize_delta) as f32).abs();
let max = unaltered.right as f32 / max_divisor;
if diff < max {
if diff < unaltered.right as f32 {
r.right -= resize_delta;
}
}
@@ -156,15 +149,13 @@ impl DefaultLayout {
OperationDirection::Down => match sizing {
Sizing::Increase => {
let diff = ((r.bottom + resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
if diff < unaltered.bottom as f32 {
r.bottom += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.bottom - resize_delta) as f32).abs();
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
if diff < unaltered.bottom as f32 {
r.bottom -= resize_delta;
}
}

View File

@@ -406,7 +406,7 @@ impl Direction for CustomLayout {
}
let (column_idx, column) = self.column_with_idx(idx);
column.map_or(false, |column| match column {
column.is_some_and(|column| match column {
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
| Column::Tertiary(ColumnSplit::Horizontal) => {
self.column_for_container_idx(idx - 1) == column_idx
@@ -420,7 +420,7 @@ impl Direction for CustomLayout {
}
let (column_idx, column) = self.column_with_idx(idx);
column.map_or(false, |column| match column {
column.is_some_and(|column| match column {
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
| Column::Tertiary(ColumnSplit::Horizontal) => {
self.column_for_container_idx(idx + 1) == column_idx

View File

@@ -25,6 +25,7 @@ pub use default_layout::DefaultLayout;
pub use direction::Direction;
pub use layout::Layout;
pub use operation_direction::OperationDirection;
pub use pathext::PathExt;
pub use rect::Rect;
pub mod animation;
@@ -37,6 +38,7 @@ pub mod default_layout;
pub mod direction;
pub mod layout;
pub mod operation_direction;
pub mod pathext;
pub mod rect;
#[derive(Clone, Debug, Serialize, Deserialize, Display, JsonSchema)]
@@ -330,6 +332,7 @@ pub enum StateQuery {
FocusedWorkspaceIndex,
FocusedContainerIndex,
FocusedWindowIndex,
FocusedWorkspaceName,
}
#[derive(

View File

@@ -0,0 +1,48 @@
use std::env;
use std::path::Component;
use std::path::PathBuf;
pub trait PathExt {
fn replace_env(&self) -> PathBuf;
}
impl PathExt for PathBuf {
fn replace_env(&self) -> PathBuf {
let mut result = PathBuf::new();
for component in self.components() {
match component {
Component::Normal(segment) => {
// Check if it starts with `$` or `$Env:`
if let Some(stripped_segment) = segment.to_string_lossy().strip_prefix('$') {
let var_name = if let Some(env_name) = stripped_segment.strip_prefix("Env:")
{
// Extract the variable name after `$Env:`
env_name
} else if stripped_segment == "HOME" {
// Special case for `$HOME`
"USERPROFILE"
} else {
// Extract the variable name after `$`
stripped_segment
};
if let Ok(value) = env::var(var_name) {
result.push(&value); // Replace with the value
} else {
result.push(segment); // Keep as-is if variable is not found
}
} else {
result.push(segment); // Keep as-is if not an environment variable
}
}
_ => {
// Add other components (e.g., root, parent) as-is
result.push(component.as_os_str());
}
}
}
result
}
}

View File

@@ -32,6 +32,7 @@ pub mod workspace;
pub mod workspace_reconciliator;
use lazy_static::lazy_static;
use monitor_reconciliator::MonitorNotification;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::fs::File;
@@ -127,6 +128,7 @@ lazy_static! {
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
]));
static ref OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST: Arc<Mutex<Vec<Regex>>> = Arc::new(Mutex::new(Vec::new()));
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()));
@@ -219,6 +221,8 @@ lazy_static! {
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
@@ -281,6 +285,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
Monitor(MonitorNotification),
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]

View File

@@ -44,6 +44,8 @@ pub struct Monitor {
#[getset(get = "pub", set = "pub")]
device_id: String,
#[getset(get = "pub", set = "pub")]
serial_number_id: Option<String>,
#[getset(get = "pub", set = "pub")]
size: Rect,
#[getset(get = "pub", set = "pub")]
work_area_size: Rect,
@@ -63,6 +65,29 @@ pub struct Monitor {
impl_ring_elements!(Monitor, Workspace);
#[derive(Serialize)]
pub struct MonitorInformation {
pub id: isize,
pub name: String,
pub device: String,
pub device_id: String,
pub serial_number_id: Option<String>,
pub size: Rect,
}
impl From<&Monitor> for MonitorInformation {
fn from(monitor: &Monitor) -> Self {
Self {
id: monitor.id,
name: monitor.name.clone(),
device: monitor.device.clone(),
device_id: monitor.device_id.clone(),
serial_number_id: monitor.serial_number_id.clone(),
size: monitor.size,
}
}
}
pub fn new(
id: isize,
size: Rect,
@@ -70,6 +95,7 @@ pub fn new(
name: String,
device: String,
device_id: String,
serial_number_id: Option<String>,
) -> Monitor {
let mut workspaces = Ring::default();
workspaces.elements_mut().push_back(Workspace::default());
@@ -79,6 +105,7 @@ pub fn new(
name,
device,
device_id,
serial_number_id,
size,
work_area_size,
work_area_offset: None,
@@ -97,6 +124,7 @@ impl Monitor {
name: "PLACEHOLDER".to_string(),
device: "".to_string(),
device_id: "".to_string(),
serial_number_id: None,
size: Default::default(),
work_area_size: Default::default(),
work_area_offset: None,
@@ -107,6 +135,13 @@ impl Monitor {
workspace_names: Default::default(),
}
}
pub fn focused_workspace_name(&self) -> Option<String> {
self.focused_workspace()
.map(|w| w.name().clone())
.unwrap_or(None)
}
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() {

View File

@@ -114,7 +114,7 @@ impl Hidden {
"WM_POWERBROADCAST event received - resume from suspend"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::ResumingFromSuspendedState,
monitor_reconciliator::MonitorNotification::ResumingFromSuspendedState,
);
LRESULT(0)
}
@@ -124,7 +124,7 @@ impl Hidden {
"WM_POWERBROADCAST event received - entering suspended state"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::EnteringSuspendedState,
monitor_reconciliator::MonitorNotification::EnteringSuspendedState,
);
LRESULT(0)
}
@@ -137,14 +137,14 @@ impl Hidden {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::SessionLocked,
monitor_reconciliator::MonitorNotification::SessionLocked,
);
}
WTS_SESSION_UNLOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::SessionUnlocked,
monitor_reconciliator::MonitorNotification::SessionUnlocked,
);
}
_ => {}
@@ -165,7 +165,7 @@ impl Hidden {
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::ResolutionScalingChanged,
monitor_reconciliator::MonitorNotification::ResolutionScalingChanged,
);
LRESULT(0)
}
@@ -179,7 +179,7 @@ impl Hidden {
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::WorkAreaChanged,
monitor_reconciliator::MonitorNotification::WorkAreaChanged,
);
}
LRESULT(0)
@@ -193,7 +193,7 @@ impl Hidden {
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::Notification::DisplayConnectionChange,
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
);
}

View File

@@ -5,13 +5,20 @@ use crate::core::Rect;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
use crate::notify_subscribers;
use crate::MonitorConfig;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::WindowManager;
use crate::WindowsApi;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
@@ -20,7 +27,9 @@ use std::sync::OnceLock;
pub mod hidden;
pub enum Notification {
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "content")]
pub enum MonitorNotification {
ResolutionScalingChanged,
WorkAreaChanged,
DisplayConnectionChange,
@@ -32,23 +41,24 @@ pub enum Notification {
static ACTIVE: AtomicBool = AtomicBool::new(true);
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
pub fn channel() -> &'static (Sender<MonitorNotification>, Receiver<MonitorNotification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
}
fn event_tx() -> Sender<Notification> {
fn event_tx() -> Sender<MonitorNotification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
fn event_rx() -> Receiver<MonitorNotification> {
channel().1.clone()
}
pub fn send_notification(notification: Notification) {
pub fn send_notification(notification: MonitorNotification) {
if event_tx().try_send(notification).is_err() {
tracing::warn!("channel is full; dropping notification")
}
@@ -89,10 +99,12 @@ pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
name,
device,
device_id,
display.serial_number_id,
)
})
.collect::<Vec<_>>())
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
#[allow(clippy::expect_used)]
Hidden::create("komorebi-hidden")?;
@@ -116,6 +128,7 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
Ok(())
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
@@ -125,7 +138,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if !ACTIVE.load_consume() {
if matches!(
notification,
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked
MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked
) {
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
@@ -140,17 +154,20 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let mut wm = wm.lock();
let initial_state = State::from(wm.as_ref());
match notification {
Notification::EnteringSuspendedState | Notification::SessionLocked => {
MonitorNotification::EnteringSuspendedState | MonitorNotification::SessionLocked => {
tracing::debug!(
"deactivating reconciliator until system resumes from suspended state or session is unlocked"
);
ACTIVE.store(false, Ordering::SeqCst);
}
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => {
MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked => {
// this is only handled above if the reconciliator is paused
}
Notification::WorkAreaChanged => {
MonitorNotification::WorkAreaChanged => {
tracing::debug!("handling work area changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
@@ -182,7 +199,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
}
}
Notification::ResolutionScalingChanged => {
MonitorNotification::ResolutionScalingChanged => {
tracing::debug!("handling resolution/scaling changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
@@ -229,7 +246,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
}
}
Notification::DisplayConnectionChange => {
MonitorNotification::DisplayConnectionChange => {
tracing::debug!("handling display connection change notification");
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
@@ -411,6 +428,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
}
}
notify_subscribers(
Notification {
event: NotificationEvent::Monitor(notification),
state: wm.as_ref().into(),
},
initial_state.has_been_modified(&wm),
)?;
}
Ok(())

View File

@@ -52,6 +52,7 @@ use crate::border_manager::STYLE;
use crate::colour::Rgb;
use crate::config_generation::WorkspaceMatchingRule;
use crate::current_virtual_desktop;
use crate::monitor::MonitorInformation;
use crate::notify_subscribers;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
@@ -1051,9 +1052,9 @@ impl WindowManager {
reply.write_all(visible_windows_state.as_bytes())?;
}
SocketMessage::MonitorInformation => {
let mut monitors = HashMap::new();
let mut monitors = vec![];
for monitor in self.monitors() {
monitors.insert(monitor.device_id(), monitor.size());
monitors.push(MonitorInformation::from(monitor));
}
let monitors_state = serde_json::to_string_pretty(&monitors)
@@ -1063,19 +1064,29 @@ impl WindowManager {
}
SocketMessage::Query(query) => {
let response = match query {
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx().to_string(),
StateQuery::FocusedWorkspaceIndex => self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx(),
StateQuery::FocusedContainerIndex => {
self.focused_workspace()?.focused_container_idx()
}
.focused_workspace_idx()
.to_string(),
StateQuery::FocusedContainerIndex => self
.focused_workspace()?
.focused_container_idx()
.to_string(),
StateQuery::FocusedWindowIndex => {
self.focused_container()?.focused_window_idx()
self.focused_container()?.focused_window_idx().to_string()
}
}
.to_string();
StateQuery::FocusedWorkspaceName => {
let focused_monitor = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?;
focused_monitor
.focused_workspace_name()
.unwrap_or_else(|| focused_monitor.focused_workspace_idx().to_string())
}
};
reply.write_all(response.as_bytes())?;
}
@@ -1305,6 +1316,8 @@ impl WindowManager {
// Initialize the new wm
wm.init()?;
wm.restore_all_windows(true)?;
// This is equivalent to StaticConfig::postload for this use case
StaticConfig::reload(config, &mut wm)?;

View File

@@ -93,8 +93,18 @@ impl WindowManager {
.map(|w| w.hwnd)
.collect::<Vec<_>>();
if w.contains_managed_window(event_hwnd)
&& !visible_hwnds.contains(&event_hwnd)
let contains_managed_window = w.contains_managed_window(event_hwnd);
// this is for an old stackbar clicking fix
if contains_managed_window && !visible_hwnds.contains(&event_hwnd) {
transparency_override = true;
}
// but we always want to handle a minimize event when transparency overrides
// are applied
if !transparency_override
&& contains_managed_window
&& matches!(event, WindowManagerEvent::Minimize(_, _))
{
transparency_override = true;
}

View File

@@ -35,13 +35,16 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::AspectRatio;
use crate::Axis;
use crate::CrossBoundaryBehaviour;
use crate::PredefinedAspectRatio;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOATING_APPLICATIONS;
use crate::FLOATING_WINDOW_TOGGLE_ASPECT_RATIO;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
@@ -49,6 +52,7 @@ use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST;
use crate::REGEX_IDENTIFIERS;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
@@ -243,7 +247,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.33`
/// The `komorebi.json` static configuration file reference for `v0.1.34`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -354,6 +358,9 @@ pub struct StaticConfig {
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change_applications: Option<Vec<MatchingRule>>,
/// Do not process EVENT_OBJECT_NAMECHANGE events as Show events for identified applications matching these title regexes
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change_title_ignore_list: Option<Vec<String>>,
/// Set monitor index preferences
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
@@ -382,6 +389,9 @@ pub struct StaticConfig {
/// HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars
#[serde(skip_serializing_if = "Option::is_none")]
pub remove_titlebar_applications: Option<Vec<MatchingRule>>,
/// Aspect ratio to resize with when toggling floating mode for a window
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_window_aspect_ratio: Option<AspectRatio>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -625,7 +635,17 @@ impl From<&WindowManager> for StaticConfig {
border_overflow_applications: None,
tray_and_multi_window_applications: None,
layered_applications: None,
object_name_change_applications: None,
object_name_change_applications: Option::from(
OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
),
object_name_change_title_ignore_list: Option::from(
OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST
.lock()
.clone()
.iter()
.map(|r| r.to_string())
.collect::<Vec<_>>(),
),
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
stackbar: None,
@@ -637,6 +657,7 @@ impl From<&WindowManager> for StaticConfig {
slow_application_identifiers: Option::from(SLOW_APPLICATION_IDENTIFIERS.lock().clone()),
bar_configurations: None,
remove_titlebar_applications: Option::from(NO_TITLEBAR.lock().clone()),
floating_window_aspect_ratio: Option::from(*FLOATING_WINDOW_TOGGLE_ASPECT_RATIO.lock()),
}
}
}
@@ -644,6 +665,10 @@ impl From<&WindowManager> for StaticConfig {
impl StaticConfig {
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
fn apply_globals(&mut self) -> Result<()> {
*FLOATING_WINDOW_TOGGLE_ASPECT_RATIO.lock() = self
.floating_window_aspect_ratio
.unwrap_or(AspectRatio::Predefined(PredefinedAspectRatio::Standard));
if let Some(monitor_index_preferences) = &self.monitor_index_preferences {
let mut preferences = MONITOR_INDEX_PREFERENCES.lock();
preferences.clone_from(monitor_index_preferences);
@@ -779,6 +804,7 @@ impl StaticConfig {
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 object_name_change_title_ignore_list = OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST.lock();
let mut layered_identifiers = LAYERED_WHITELIST.lock();
let mut transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
let mut slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
@@ -805,6 +831,17 @@ impl StaticConfig {
)?;
}
if let Some(regexes) = &mut self.object_name_change_title_ignore_list {
let mut updated = vec![];
for r in regexes {
if let Ok(regex) = Regex::new(r) {
updated.push(regex);
}
}
*object_name_change_title_ignore_list = updated;
}
if let Some(rules) = &mut self.layered_applications {
populate_rules(rules, &mut layered_identifiers, &mut regex_identifiers)?;
}
@@ -1014,6 +1051,10 @@ impl StaticConfig {
Ok(())
}
pub fn read_raw(raw: &str) -> Result<Self> {
Ok(serde_json::from_str(raw)?)
}
pub fn read(path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
@@ -1205,6 +1246,9 @@ impl StaticConfig {
value.apply_globals()?;
if let Some(monitors) = value.monitors {
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_matching_rules.clear();
for (i, monitor) in monitors.iter().enumerate() {
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
@@ -1223,8 +1267,6 @@ 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 {

View File

@@ -15,6 +15,7 @@ use crate::focus_manager;
use crate::stackbar_manager;
use crate::windows_api;
use crate::AnimationStyle;
use crate::FLOATING_WINDOW_TOGGLE_ASPECT_RATIO;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
use std::collections::HashMap;
@@ -296,6 +297,38 @@ impl RenderDispatcher for TransparencyRenderDispatcher {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum AspectRatio {
/// A predefined aspect ratio
Predefined(PredefinedAspectRatio),
/// A custom W:H aspect ratio
Custom(i32, i32),
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum PredefinedAspectRatio {
/// 21:9
Ultrawide,
/// 16:9
Widescreen,
/// 4:3
Standard,
}
impl AspectRatio {
pub fn width_and_height(self) -> (i32, i32) {
match self {
AspectRatio::Predefined(predefined) => match predefined {
PredefinedAspectRatio::Ultrawide => (21, 9),
PredefinedAspectRatio::Widescreen => (16, 9),
PredefinedAspectRatio::Standard => (4, 3),
},
AspectRatio::Custom(w, h) => (w, h),
}
}
}
impl Window {
pub const fn hwnd(self) -> HWND {
HWND(windows_api::as_ptr!(self.hwnd))
@@ -369,15 +402,21 @@ impl Window {
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
let (aspect_ratio_width, aspect_ratio_height) = FLOATING_WINDOW_TOGGLE_ASPECT_RATIO
.lock()
.width_and_height();
let target_height = work_area.bottom / 2;
let target_width = (target_height * aspect_ratio_width) / aspect_ratio_height;
let x = work_area.left + ((work_area.right - target_width) / 2);
let y = work_area.top + ((work_area.bottom - target_height) / 2);
self.set_position(
&Rect {
left: work_area.left + ((work_area.right - half_width) / 2),
top: work_area.top + ((work_area.bottom - half_weight) / 2),
right: half_width,
bottom: half_weight,
left: x,
top: y,
right: target_width,
bottom: target_height,
},
true,
)
@@ -872,7 +911,11 @@ fn window_is_eligible(
let known_layered_hwnds = transparency_manager::known_hwnds();
allow_layered = if known_layered_hwnds.contains(&hwnd) {
allow_layered = if known_layered_hwnds.contains(&hwnd)
// we always want to process hide events for windows with transparency, even on other
// monitors, because we don't want to be left with ghost tiles
|| matches!(event, Some(WindowManagerEvent::Hide(_, _)))
{
debug.allow_layered_transparency = true;
true
} else {
@@ -924,12 +967,12 @@ fn window_is_eligible(
}
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
// Get a lot of dupe events coming through that make the redrawing go crazy
// on FocusChange events if I don't filter out this one. But, if we are
// allowing a specific layered window on the whitelist (like Steam), it should
// pass this check
&& (allow_layered || !ex_style.contains(ExtendedWindowStyle::LAYERED))
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
// Get a lot of dupe events coming through that make the redrawing go crazy
// on FocusChange events if I don't filter out this one. But, if we are
// allowing a specific layered window on the whitelist (like Steam), it should
// pass this check
&& (allow_layered || !ex_style.contains(ExtendedWindowStyle::LAYERED))
|| managed_override
{
return true;

View File

@@ -1888,21 +1888,10 @@ impl WindowManager {
if let Ok(focused_workspace) = self.focused_workspace_mut() {
if let Some(window) = focused_workspace.maximized_window() {
window.focus(mouse_follows_focus)?;
// (alex-ds13): @LGUG2Z Why was this being done below on the monocle?
// Should it really be done?
//
// WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
// window.hwnd,
// )?)?;
cross_monitor_monocle_or_max = true;
} else if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(mouse_follows_focus)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
window.hwnd,
)?)?;
cross_monitor_monocle_or_max = true;
}
} else {

View File

@@ -9,6 +9,7 @@ use crate::window::should_act;
use crate::window::Window;
use crate::winevent::WinEvent;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST;
use crate::REGEX_IDENTIFIERS;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
@@ -176,6 +177,8 @@ impl WindowManagerEvent {
// [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
let object_name_change_title_ignore_list =
OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let title = &window.title().ok()?;
@@ -183,7 +186,7 @@ impl WindowManagerEvent {
let class = &window.class().ok()?;
let path = &window.path().ok()?;
let should_trigger_show = should_act(
let mut should_trigger_show = should_act(
title,
exe_name,
class,
@@ -193,6 +196,14 @@ impl WindowManagerEvent {
)
.is_some();
if should_trigger_show {
for r in &*object_name_change_title_ignore_list {
if r.is_match(title) {
should_trigger_show = false;
}
}
}
// 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

View File

@@ -283,6 +283,7 @@ impl WindowsApi {
name,
device,
device_id,
display.serial_number_id,
);
let mut index_preference = None;
@@ -936,6 +937,7 @@ impl WindowsApi {
name,
device,
device_id,
display.serial_number_id,
);
return Ok(monitor);

View File

@@ -333,10 +333,6 @@ impl Workspace {
}
if let Some(updated_layout) = updated_layout {
if !matches!(updated_layout, Layout::Default(DefaultLayout::BSP)) {
self.set_layout_flip(None);
}
self.set_layout(updated_layout);
}
}

29
komorebi/tests/compat.rs Normal file
View File

@@ -0,0 +1,29 @@
use komorebi::StaticConfig;
#[test]
fn backwards_compat() {
let root = vec!["0.1.17", "0.1.18", "0.1.19"];
let docs = vec![
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27", "0.1.28",
"0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33",
];
let mut versions = vec![];
let client = reqwest::blocking::Client::new();
for version in root {
let request = client.get(format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v{version}/komorebi.example.json")).header("User-Agent", "komorebi-backwards-compat-test").build().unwrap();
versions.push((version, client.execute(request).unwrap().text().unwrap()));
}
for version in docs {
let request = client.get(format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v{version}/docs/komorebi.example.json")).header("User-Agent", "komorebi-backwards-compat-test").build().unwrap();
versions.push((version, client.execute(request).unwrap().text().unwrap()));
}
for (version, config) in versions {
println!("{version}");
StaticConfig::read_raw(&config).unwrap();
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.33"
version = "0.1.34"
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.33"
version = "0.1.34"
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

@@ -1175,7 +1175,7 @@ enum SubCommand {
#[clap(hide = true)]
#[clap(arg_required_else_help = true)]
LoadCustomLayout(LoadCustomLayout),
/// Flip the layout on the focused workspace (BSP only)
/// Flip the layout on the focused workspace
#[clap(arg_required_else_help = true)]
FlipLayout(FlipLayout),
/// Promote the focused window to the top of the tree

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "KomobarConfig",
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.33`",
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.34`",
"type": "object",
"required": [
"left_widgets",
@@ -36,6 +36,10 @@
"description": "Enable the Battery widget",
"type": "boolean"
},
"hide_on_full_charge": {
"description": "Hide the widget if the battery is at full charge",
"type": "boolean"
},
"label_prefix": {
"description": "Display label prefix",
"oneOf": [
@@ -194,6 +198,38 @@
}
},
"additionalProperties": false
},
{
"description": "Custom format with modifiers",
"type": "object",
"required": [
"CustomModifiers"
],
"properties": {
"CustomModifiers": {
"description": "Custom format with additive modifiers for integer format specifiers",
"type": "object",
"required": [
"format",
"modifiers"
],
"properties": {
"format": {
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "string"
},
"modifiers": {
"description": "Additive modifiers for integer format specifiers (e.g. { \"%U\": 1 } to increment the zero-indexed week number by 1)",
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
}
}
}
},
"additionalProperties": false
}
]
},
@@ -719,6 +755,13 @@
"TwelveHour"
]
},
{
"description": "Twelve-hour format (without seconds)",
"type": "string",
"enum": [
"TwelveHourWithoutSeconds"
]
},
{
"description": "Twenty-four-hour format (with seconds)",
"type": "string",
@@ -726,6 +769,27 @@
"TwentyFourHour"
]
},
{
"description": "Twenty-four-hour format (without seconds)",
"type": "string",
"enum": [
"TwentyFourHourWithoutSeconds"
]
},
{
"description": "Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
"type": "string",
"enum": [
"BinaryCircle"
]
},
{
"description": "Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
"type": "string",
"enum": [
"BinaryRectangle"
]
},
{
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "object",
@@ -1182,6 +1246,11 @@
}
]
},
"height": {
"description": "Bar height (default: 50)",
"type": "number",
"format": "float"
},
"icon_scale": {
"description": "Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)",
"type": "number",
@@ -1214,6 +1283,10 @@
"description": "Enable the Battery widget",
"type": "boolean"
},
"hide_on_full_charge": {
"description": "Hide the widget if the battery is at full charge",
"type": "boolean"
},
"label_prefix": {
"description": "Display label prefix",
"oneOf": [
@@ -1372,6 +1445,38 @@
}
},
"additionalProperties": false
},
{
"description": "Custom format with modifiers",
"type": "object",
"required": [
"CustomModifiers"
],
"properties": {
"CustomModifiers": {
"description": "Custom format with additive modifiers for integer format specifiers",
"type": "object",
"required": [
"format",
"modifiers"
],
"properties": {
"format": {
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "string"
},
"modifiers": {
"description": "Additive modifiers for integer format specifiers (e.g. { \"%U\": 1 } to increment the zero-indexed week number by 1)",
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
}
}
}
},
"additionalProperties": false
}
]
},
@@ -1897,6 +2002,13 @@
"TwelveHour"
]
},
{
"description": "Twelve-hour format (without seconds)",
"type": "string",
"enum": [
"TwelveHourWithoutSeconds"
]
},
{
"description": "Twenty-four-hour format (with seconds)",
"type": "string",
@@ -1904,6 +2016,27 @@
"TwentyFourHour"
]
},
{
"description": "Twenty-four-hour format (without seconds)",
"type": "string",
"enum": [
"TwentyFourHourWithoutSeconds"
]
},
{
"description": "Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
"type": "string",
"enum": [
"BinaryCircle"
]
},
{
"description": "Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
"type": "string",
"enum": [
"BinaryRectangle"
]
},
{
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "object",
@@ -2020,26 +2153,14 @@
]
}
},
"max_label_width": {
"description": "Max label width before text truncation (default: 400.0)",
"type": "number",
"format": "float"
},
"monitor": {
"description": "Monitor options",
"type": "object",
"required": [
"index"
],
"properties": {
"index": {
"description": "Komorebi monitor index of the monitor on which to render the bar",
"type": "integer",
"format": "uint",
"minimum": 0.0
"margin": {
"description": "Bar margin. Use one value for all sides or use a grouped margin for horizontal and/or vertical definition which can each take a single value for a symmetric margin or two values for each side, i.e.: ```json \"margin\": { \"horizontal\": 10 } ``` or: ```json \"margin\": { \"vertical\": [top, bottom] } ``` You can also set individual margin on each side like this: ```json \"margin\": { \"top\": 10, \"bottom\": 10, \"left\": 10, \"right\": 10, } ``` By default, margin is set to 0 on all sides.",
"anyOf": [
{
"type": "number",
"format": "float"
},
"work_area_offset": {
"description": "Automatically apply a work area offset for this monitor to accommodate the bar",
{
"type": "object",
"required": [
"bottom",
@@ -2049,28 +2170,225 @@
],
"properties": {
"bottom": {
"description": "The bottom point in a Win32 Rect",
"type": "integer",
"format": "int32"
"type": "number",
"format": "float"
},
"left": {
"description": "The left point in a Win32 Rect",
"type": "integer",
"format": "int32"
"type": "number",
"format": "float"
},
"right": {
"description": "The right point in a Win32 Rect",
"type": "integer",
"format": "int32"
"type": "number",
"format": "float"
},
"top": {
"description": "The top point in a Win32 Rect",
"type": "integer",
"format": "int32"
"type": "number",
"format": "float"
}
}
},
{
"type": "object",
"properties": {
"horizontal": {
"anyOf": [
{
"type": "number",
"format": "float"
},
{
"type": "array",
"items": [
{
"type": "number",
"format": "float"
},
{
"type": "number",
"format": "float"
}
],
"maxItems": 2,
"minItems": 2
}
]
},
"vertical": {
"anyOf": [
{
"type": "number",
"format": "float"
},
{
"type": "array",
"items": [
{
"type": "number",
"format": "float"
},
{
"type": "number",
"format": "float"
}
],
"maxItems": 2,
"minItems": 2
}
]
}
}
}
}
]
},
"max_label_width": {
"description": "Max label width before text truncation (default: 400.0)",
"type": "number",
"format": "float"
},
"monitor": {
"description": "The monitor index or the full monitor options",
"anyOf": [
{
"description": "The monitor index where you want the bar to show",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
{
"description": "The full monitor options with the index and an optional work_area_offset",
"type": "object",
"required": [
"index"
],
"properties": {
"index": {
"description": "Komorebi monitor index of the monitor on which to render the bar",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"work_area_offset": {
"description": "Automatically apply a work area offset for this monitor to accommodate the bar",
"type": "object",
"required": [
"bottom",
"left",
"right",
"top"
],
"properties": {
"bottom": {
"description": "The bottom point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"left": {
"description": "The left point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"right": {
"description": "The right point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"top": {
"description": "The top point in a Win32 Rect",
"type": "integer",
"format": "int32"
}
}
}
}
}
]
},
"padding": {
"description": "Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or vertical definition which can each take a single value for a symmetric padding or two values for each side, i.e.: ```json \"padding\": { \"horizontal\": 10 } ``` or: ```json \"padding\": { \"horizontal\": [left, right] } ``` You can also set individual padding on each side like this: ```json \"padding\": { \"top\": 10, \"bottom\": 10, \"left\": 10, \"right\": 10, } ``` By default, padding is set to 10 on all sides.",
"anyOf": [
{
"type": "number",
"format": "float"
},
{
"type": "object",
"required": [
"bottom",
"left",
"right",
"top"
],
"properties": {
"bottom": {
"type": "number",
"format": "float"
},
"left": {
"type": "number",
"format": "float"
},
"right": {
"type": "number",
"format": "float"
},
"top": {
"type": "number",
"format": "float"
}
}
},
{
"type": "object",
"properties": {
"horizontal": {
"anyOf": [
{
"type": "number",
"format": "float"
},
{
"type": "array",
"items": [
{
"type": "number",
"format": "float"
},
{
"type": "number",
"format": "float"
}
],
"maxItems": 2,
"minItems": 2
}
]
},
"vertical": {
"anyOf": [
{
"type": "number",
"format": "float"
},
{
"type": "array",
"items": [
{
"type": "number",
"format": "float"
},
{
"type": "number",
"format": "float"
}
],
"maxItems": 2,
"minItems": 2
}
]
}
}
}
]
},
"position": {
"description": "Bar positioning options",
@@ -2145,6 +2463,10 @@
"description": "Enable the Battery widget",
"type": "boolean"
},
"hide_on_full_charge": {
"description": "Hide the widget if the battery is at full charge",
"type": "boolean"
},
"label_prefix": {
"description": "Display label prefix",
"oneOf": [
@@ -2303,6 +2625,38 @@
}
},
"additionalProperties": false
},
{
"description": "Custom format with modifiers",
"type": "object",
"required": [
"CustomModifiers"
],
"properties": {
"CustomModifiers": {
"description": "Custom format with additive modifiers for integer format specifiers",
"type": "object",
"required": [
"format",
"modifiers"
],
"properties": {
"format": {
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "string"
},
"modifiers": {
"description": "Additive modifiers for integer format specifiers (e.g. { \"%U\": 1 } to increment the zero-indexed week number by 1)",
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
}
}
}
},
"additionalProperties": false
}
]
},
@@ -2828,6 +3182,13 @@
"TwelveHour"
]
},
{
"description": "Twelve-hour format (without seconds)",
"type": "string",
"enum": [
"TwelveHourWithoutSeconds"
]
},
{
"description": "Twenty-four-hour format (with seconds)",
"type": "string",
@@ -2835,6 +3196,27 @@
"TwentyFourHour"
]
},
{
"description": "Twenty-four-hour format (without seconds)",
"type": "string",
"enum": [
"TwentyFourHourWithoutSeconds"
]
},
{
"description": "Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
"type": "string",
"enum": [
"BinaryCircle"
]
},
{
"description": "Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
"type": "string",
"enum": [
"BinaryRectangle"
]
},
{
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "object",

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.33`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.34`",
"type": "object",
"properties": {
"animation": {
@@ -642,6 +642,53 @@
]
}
},
"floating_window_aspect_ratio": {
"description": "Aspect ratio to resize with when toggling floating mode for a window",
"anyOf": [
{
"description": "A predefined aspect ratio",
"oneOf": [
{
"description": "21:9",
"type": "string",
"enum": [
"Ultrawide"
]
},
{
"description": "16:9",
"type": "string",
"enum": [
"Widescreen"
]
},
{
"description": "4:3",
"type": "string",
"enum": [
"Standard"
]
}
]
},
{
"description": "A custom W:H aspect ratio",
"type": "array",
"items": [
{
"type": "integer",
"format": "int32"
},
{
"type": "integer",
"format": "int32"
}
],
"maxItems": 2,
"minItems": 2
}
]
},
"focus_follows_mouse": {
"description": "END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead",
"oneOf": [
@@ -1457,6 +1504,13 @@
]
}
},
"object_name_change_title_ignore_list": {
"description": "Do not process EVENT_OBJECT_NAMECHANGE events as Show events for identified applications matching these title regexes",
"type": "array",
"items": {
"type": "string"
}
},
"remove_titlebar_applications": {
"description": "HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars",
"type": "array",