Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z
559fa55b9f hardcoded cursor at top left 2024-11-19 20:49:39 -08:00
108 changed files with 3553 additions and 11188 deletions

View File

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

View File

@@ -1,47 +0,0 @@
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'
});
}

125
.github/workflows/sponsor-check.yaml vendored Normal file
View File

@@ -0,0 +1,125 @@
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,8 +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
- uses: houseabsolute/actions-rust-cross@v0
with:
command: "build"
target: ${{ matrix.platform.target }}
@@ -200,7 +199,7 @@ jobs:
needs: release
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal2009/winget-releaser@main
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: LGUG2Z.komorebi
token: ${{ secrets.WINGET_TOKEN }}

1555
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,9 @@ chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.30"
egui_extras = "0.30"
dirs = "6"
eframe = "0.29"
egui_extras = "0.29"
dirs = "5"
dunce = "1"
hotwatch = "0.5"
schemars = "0.8"
@@ -31,33 +31,29 @@ tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.33"
sysinfo = "0.31"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "376523b9e1321e033b0b0ed0e6fa75a072b46ad9" }
windows-numerics = { version = "0.1" }
windows-implement = { version = "0.59" }
windows-interface = { version = "0.59" }
windows-core = { version = "0.60" }
shadow-rs = "0.38"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.35"
which = "7"
[workspace.dependencies.windows]
version = "0.60"
version = "0.58"
features = [
"implement",
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",

115
README.md
View File

@@ -29,8 +29,6 @@ Tiling Window Management for Windows.
![screenshot](https://user-images.githubusercontent.com/13164844/184027064-f5a6cec2-2865-4d65-a549-a1f1da589abf.png)
## Overview
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
[Desktop Window
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
@@ -52,8 +50,6 @@ _komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflow
[configuration schema reference](https://komorebi.lgug2z.com/schema) and a
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
## Community
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
_komorebi_-related discussion, help, troubleshooting etc. If you have any
specific feature requests or bugs to report, please create an issue in this
@@ -61,62 +57,28 @@ repository.
There is a [YouTube
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
_komorebi_ development videos, feature previews and release overviews. Subscribing
to the channel (which is monetized as part of the YouTube Partner Program) and
watching videos is a really simple and passive way to contribute financially to
the development and maintenance of _komorebi_.
_komorebi_ development videos. If you would like to be notified of upcoming
videos please subscribe and turn on notifications.
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
showcases the many awesome projects that exist in the _komorebi_ ecosystem.
## Licensing for Personal Use
`komorebi` is licensed under the [Komorebi 1.0.0
license](https://github.com/LGUG2Z/komorebi-license), which is a fork of the
[PolyForm Strict 1.0.0
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
this means that you are free to do whatever you want with `komorebi` for
personal use other than redistribution, or distribution of new works (i.e.
hard-forks) based on the software.
Anyone is free to make their own fork of `komorebi` with changes intended either
for personal use or for integration back upstream via pull requests.
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
not permit any kind of commercial use (i.e. using `komorebi` at work).
## Sponsorship for Personal Use
_komorebi_ is a free and educational source project, and one that encourages you
to make charitable donations if you find the software to be useful and have the
_komorebi_ is a free and source-available project, and one that encourages you to
make charitable donations if you find the software to be useful and have the
financial means.
I encourage you to make a charitable donation to the [Palestine Children's
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or contributing
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
me on GitHub.
[GitHub Sponsors is enabled for this
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
the Discord server, get shout outs at the end of _komorebi_-related videos on
YouTube, and gain the ability to submit feature requests on the issue tracker.
project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have
anything specific to offer besides my gratitude and shout outs at the end of
_komorebi_ live development videos and tutorials.
If you would like to tip or sponsor the project but are unable to use GitHub
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
## Licensing for Commercial Use
A dedicated Individual Commercial Use License is available for those who want to
use `komorebi` at work.
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
for the licensed individual only, for the duration of a valid paid license
subscription only. All provisions and restrictions enumerated in the [Komorebi
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
More information, pricing and purchase links for Individual Commercial Use
Licenses [can be found here](https://lgug2z.com/software/komorebi).
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z).
# Installation
@@ -163,11 +125,7 @@ https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-
# Contribution Guidelines
If you would like to contribute to `komorebi` please take the time to carefully
read the guidelines below.
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
code contributions to `komorebi` are licensed.
If you would like to contribute to `komorebi` please take the time to carefully read the guidelines below.
## Commit hygiene
@@ -177,8 +135,8 @@ code contributions to `komorebi` are licensed.
- Use `git cz` with
the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare
commit messages
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for
the changes being committed
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the
changes being committed
## PRs should contain only a single feature or bug fix
@@ -217,8 +175,7 @@ This includes but is not limited to:
- All `komorebic` commands
- The `komorebi.json` schema
- The [
`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
- The [`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
schema
No user should ever find that their configuration file has stopped working after upgrading to a new version
@@ -234,6 +191,27 @@ ability for users to specify colours in `komorebi.json` in Hex format alongside
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
required.
## License
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
is a fork of the [PolyForm Strict 1.0.0
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
this means that you are free to do whatever you want with `komorebi` for
personal use other than redistribution, or distribution of new works (ie.
hard-forks) based on the software.
Anyone is free to make their own fork of `komorebi` with changes intended
either for personal use or for integration back upstream via pull requests.
The [Komorebi 1.0.0 License](./LICENSE.md) does not permit any kind of
commercial use.
A dedicated license and EULA will be introduced in 2025 for both commercial and
noncommercial organizations.
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
code contributions to `komorebi` are licensed.
# Development
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
@@ -242,13 +220,13 @@ the IDE for completions and navigation:
- Set `Expand declarative macros`
to `Use new engine` under "Settings > Langauges & Frameworks > Rust"
- Enable the following experimental features:
- `org.rust.cargo.evaluate.build.scripts`
- `org.rust.macros.proc`
- `org.rust.cargo.evaluate.build.scripts`
- `org.rust.macros.proc`
# Logs and Debugging
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or
overwritten, so it will keep growing until it is deleted by the user.
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or overwritten, so it will keep
growing until it is deleted by the user.
Whenever running the `komorebic stop` command or sending a Ctrl-C signal to `komorebi` directly, the `komorebi` process
ensures that all hidden windows are restored before termination.
@@ -389,7 +367,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.34"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.30"}
use anyhow::Result;
use komorebi_client::Notification;
@@ -464,17 +442,12 @@ programming languages.
# Appreciations
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its
never-ending development
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its never-ending development
- Thank you to [@sitiom](https://github.com/sitiom) for
being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
- Thank you to [@sitiom](https://github.com/sitiom) for being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught
me more than I can ever hope to repay
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught me more than I can ever hope to repay
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling
window management on Windows with me and having an excellent spirit of collaboration
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling window management on Windows with me and having an excellent spirit of collaboration
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring
the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`

View File

@@ -3,18 +3,13 @@
```
Set the duration for movement animations in ms
Usage: komorebic.exe animation-duration [OPTIONS] <DURATION>
Usage: komorebic.exe animation-duration <DURATION>
Arguments:
<DURATION>
Desired animation durations in ms
Options:
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the duration to. If not specified, sets global duration
[possible values: movement, transparency]
-h, --help
Print help

View File

@@ -10,14 +10,8 @@ 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]
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the style to. If not specified, sets global style
[possible values: movement, transparency]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo,
ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
-h, --help
Print help

View File

@@ -3,18 +3,13 @@
```
Enable or disable movement animations
Usage: komorebic.exe animation [OPTIONS] <BOOLEAN_STATE>
Usage: komorebic.exe animation <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the state to. If not specified, sets global state
[possible values: movement, transparency]
-h, --help
Print help

View File

@@ -3,12 +3,9 @@
```
Check komorebi configuration and related files for common errors
Usage: komorebic.exe check [OPTIONS]
Usage: komorebic.exe check
Options:
-k, --komorebi-config <KOMOREBI_CONFIG>
Path to a static configuration JSON file
-h, --help
Print help

View File

@@ -1,12 +0,0 @@
# close-workspace
```
Close the focused workspace (must be empty and unnamed)
Usage: komorebic.exe close-workspace
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# cycle-stack-index
```
Cycle the index of the focused window in the focused stack in the specified cycle direction
Usage: komorebic.exe cycle-stack-index <CYCLE_DIRECTION>
Arguments:
<CYCLE_DIRECTION>
[possible values: previous, next]
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# eager-focus
```
Focus the first managed window matching the given exe
Usage: komorebic.exe eager-focus <EXE>
Arguments:
<EXE>
Case-sensitive exe identifier
Options:
-h, --help
Print help
```

View File

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

View File

@@ -1,12 +0,0 @@
# enforce-workspace-rules
```
Enforce all workspace rules, including initial workspace rules that have already been applied
Usage: komorebic.exe enforce-workspace-rules
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# flip-layout
```
Flip the layout on the focused workspace
Flip the layout on the focused workspace (BSP only)
Usage: komorebic.exe flip-layout <AXIS>

View File

@@ -1,12 +0,0 @@
# focus-monitor-at-cursor
```
Focus the monitor at the current cursor location
Usage: komorebic.exe focus-monitor-at-cursor
Options:
-h, --help
Print help
```

View File

@@ -1,24 +0,0 @@
# kill
```
Kill background processes started by komorebic
Usage: komorebic.exe kill [OPTIONS]
Options:
--whkd
Kill whkd if it is running as a background process
--ahk
Kill ahk if it is running as a background process
--bar
Kill komorebi-bar if it is running as a background process
--masir
Kill masir if it is running as a background process
-h, --help
Print help
```

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
Arguments:
<STATE_QUERY>
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name]
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index]
Options:
-h, --help

View File

@@ -1,18 +0,0 @@
# stackbar-mode
```
Set the stackbar mode
Usage: komorebic.exe stackbar-mode <MODE>
Arguments:
<MODE>
Desired stackbar mode
[possible values: always, never, on-stack]
Options:
-h, --help
Print help
```

View File

@@ -24,12 +24,6 @@ Options:
--bar
Start komorebi-bar in a background process
--masir
Start masir in a background process for focus-follows-mouse
--clean-state
Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
-h, --help
Print help

View File

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

View File

@@ -1,12 +0,0 @@
# toggle-window-based-work-area-offset
```
Toggle application of the window-based work area offset for the focused workspace
Usage: komorebic.exe toggle-window-based-work-area-offset
Options:
-h, --help
Print help
```

View File

@@ -1,8 +1,7 @@
# 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,12 +0,0 @@
# toggle-workspace-layer
```
Toggle between the Tiling and Floating layers on the focused workspace
Usage: komorebic.exe toggle-workspace-layer
Options:
-h, --help
Print help
```

View File

@@ -1,8 +1,7 @@
# 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,15 +26,5 @@ 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

@@ -75,7 +75,7 @@ solo developer.
If you choose to use the active window border, you can set different colours to
give you visual queues when you are focused on a single window, a stack of
windows, or a window that is in monocle mode.
windows, or a window that is in monocole mode.
The example colours given are blue single, green for stack and pink for
monocle.
@@ -181,10 +181,10 @@ The `grid` layout does not support resizing windows tiles.
key bindings go to the left of the colon, and shell commands go to the right of the
colon.
As of [`v0.2.4`](https://github.com/LGUG2Z/whkd/releases/tag/v0.2.4), `whkd` can override most of Microsoft's
limitations on hotkey bindings that include the `win` key. However, you will still need
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
`win + l` from locking the operating system.
Please remember that `whkd` does not support overriding Microsoft's limitations
on hotkey bindings that include the `Windows` key. If this is important to you,
I recommend using [AutoHotKey](https://autohotkey.com) to set up your key
bindings for `komorebic` commands instead.
```
{% include "./whkdrc.sample" %}
@@ -203,7 +203,7 @@ It is also possible to change a hotkey behavior depending on which application h
alt + n [
# ProcessName as shown by `Get-Process`
Firefox : echo "hello firefox"
# Spaces are fine, no quotes required
Google Chrome : echo "hello chrome"
]
@@ -254,5 +254,5 @@ stackbars as well as the status bar.
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
All [Catppuccin palette variants](https://catppuccin.com/)
and [most Base16 palette variants](https://tinted-theming.github.io/tinted-gallery/)
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
are available as themes.

View File

@@ -1,7 +1,5 @@
![screenshot](https://user-images.githubusercontent.com/13164844/184027064-f5a6cec2-2865-4d65-a549-a1f1da589abf.png)
## Overview
`komorebi` is a tiling window manager that works as an extension to Microsoft's
[Desktop Window
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
@@ -17,63 +15,12 @@ system and desktop environment by default. Users are free to make such
modifications in their own configuration files for `komorebi`, but these will
always remain opt-in and off-by-default.
## Community
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
`komorebi`-related discussion, help, troubleshooting etc.
`komorebi`-related discussion, help, troubleshooting etc. If you have any
specific feature requests or bugs to report, please create an issue on
[GitHub](https://github.com/LGUG2Z/komorebi).
There is a [YouTube
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
`komorebi` development videos, feature previews and release overviews. Subscribing
to the channel (which is monetized as part of the YouTube Partner Program) and
watching videos is a really simple and passive way to contribute financially to
the development and maintenance of `komorebi`.
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
showcases the many awesome projects that exist in the `komorebi` ecosystem.
## Licensing for Personal Use
`komorebi` is licensed under the [Komorebi 1.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
of the [PolyForm Strict 1.0.0 license](https://polyformproject.org/licenses/strict/1.0.0). On a high level this means
that you are free to do whatever you want with `komorebi` for personal use other than redistribution, or distribution of
new works (i.e. hard-forks) based on the software.
Anyone is free to make their own fork of `komorebi` with changes intended either for personal use or for integration
back upstream via pull requests.
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
i.e. using `komorebi` at work).
## Sponsorship for Personal Use
`komorebi` is a free and educational source project, and one that encourages you
to make charitable donations if you find the software to be useful and have the
financial means.
I encourage you to make a charitable donation to the [Palestine Children's
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
me on GitHub.
[GitHub Sponsors is enabled for this
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
the Discord server, get shout-outs at the end of _komorebi_-related videos on
YouTube, and gain the ability to submit feature requests on the issue tracker.
If you would like to tip or sponsor the project but are unable to use GitHub
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
## Licensing for Commercial Use
A dedicated Individual Commercial Use License is available for those who want to
use `komorebi` at work.
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
for the licensed individual only, for the duration of a valid paid license
subscription only. All provisions and restrictions enumerated in the [Komorebi
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
More information, pricing and purchase links for Individual Commercial Use
Licenses [can be found here](https://lgug2z.com/software/komorebi).
There is also a [YouTube
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg?sub_confirmation=1)
where I share `komorebi` live programming videos and tutorial videos.

View File

@@ -1,6 +1,14 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.bar.json",
"monitor": 0,
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {
"left": 0,
"top": 40,
"right": 0,
"bottom": 40
}
},
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",
@@ -25,11 +33,6 @@
}
],
"right_widgets": [
{
"Update": {
"enable": true
}
},
{
"Media": {
"enable": true
@@ -70,4 +73,4 @@
}
}
]
}
}

View File

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

View File

@@ -1,5 +1,4 @@
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
export RUST_BACKTRACE := "full"
clean:
@@ -24,15 +23,6 @@ install-target target:
install:
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
build-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
build-target target:
cargo +stable build --release --package {{ target }} --locked
build:
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
run target:
cargo +stable run --bin {{ target }} --locked
@@ -55,15 +45,13 @@ docgen:
cargo run --package komorebic -- docgen
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
jsonschema:
schemagen:
cargo run --package komorebic -- static-config-schema > schema.json
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
cargo run --package komorebi-bar -- --schema > schema.bar.json
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
# this part is run in a nix shell because python is a nightmare
schemagen:
rm -rf static-config-docs bar-config-docs
mkdir -p static-config-docs bar-config-docs
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
rm -Force .\bar-config-docs\schema.html
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.35"
version = "0.1.31"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -16,23 +16,22 @@ crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = "0.8"
egui-phosphor = "0.7"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.32"
netdev = "0.31"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
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::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -14,7 +15,6 @@ 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;
@@ -22,8 +22,6 @@ 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
@@ -32,19 +30,37 @@ pub struct BatteryConfig {
impl From<BatteryConfig> for Battery {
fn from(value: BatteryConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
let manager = Manager::new().unwrap();
let mut last_state = String::new();
let mut state = None;
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(mut batteries) = manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => state = Some(BatteryState::Charging),
State::Discharging => state = Some(BatteryState::Discharging),
_ => {}
}
last_state = match prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
Self {
enable: value.enable,
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
manager: Manager::new().unwrap(),
last_state: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
state: BatteryState::Discharging,
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
manager,
last_state,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
state: state.unwrap_or(BatteryState::Discharging),
last_updated: Instant::now(),
}
}
}
@@ -56,7 +72,6 @@ pub enum BatteryState {
pub struct Battery {
pub enable: bool,
hide_on_full_charge: bool,
manager: Manager,
pub state: BatteryState,
data_refresh_interval: u64,
@@ -76,22 +91,17 @@ 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,
_ => {}
}
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}%"),
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
@@ -114,12 +124,19 @@ impl BarWidget for Battery {
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
};
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -127,26 +144,15 @@ impl BarWidget for Battery {
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()
},
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
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)
}
}
config.apply_on_widget(true, ui, |ui| {
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
});
}
}

View File

@@ -1,6 +1,5 @@
use crate::render::Grouping;
use crate::widget::WidgetConfig;
use crate::DEFAULT_PADDING;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
@@ -13,73 +12,19 @@ use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.35`
/// The `komorebi.bar.json` configuration file reference for `v0.1.31`
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>,
/// The monitor index or the full monitor options
pub monitor: MonitorConfigOrIndex,
/// Monitor options
pub monitor: MonitorConfig,
/// Font family
pub font_family: Option<String>,
/// Font size (default: 12.5)
pub font_size: Option<f32>,
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
pub icon_scale: Option<f32>,
/// Max label width before text truncation (default: 400.0)
pub max_label_width: Option<f32>,
/// Theme
@@ -92,8 +37,6 @@ pub struct KomobarConfig {
pub grouping: Option<Grouping>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right)
pub center_widgets: Option<Vec<WidgetConfig>>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
@@ -143,15 +86,6 @@ 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
@@ -160,154 +94,6 @@ 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)?;
@@ -318,10 +104,7 @@ impl KomobarConfig {
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position {
x: DEFAULT_PADDING,
y: DEFAULT_PADDING,
},
inner_margin: Position { x: 10.0, y: 10.0 },
});
}
@@ -366,7 +149,7 @@ pub enum KomobarTheme {
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
},
@@ -402,17 +185,3 @@ pub enum LabelPrefix {
/// Show an icon and text
IconAndText,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub enum DisplayFormat {
/// Show only icon
Icon,
/// Show only text
Text,
/// Show an icon and text for the selected element, and text on the rest
TextAndIconOnSelected,
/// Show both icon and text
IconAndText,
/// Show an icon and text for the selected element, and icons on the rest
IconAndTextOnSelected,
}

View File

@@ -1,12 +1,13 @@
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::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -29,18 +30,17 @@ pub struct CpuConfig {
impl From<CpuConfig> for Cpu {
fn from(value: CpuConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
let mut system =
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
system.refresh_cpu_usage();
Self {
enable: value.enable,
system: System::new_with_specifics(
RefreshKind::default().without_memory().without_processes(),
),
data_refresh_interval,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
last_updated: Instant::now(),
}
}
}
@@ -74,6 +74,13 @@ impl BarWidget for Cpu {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -81,7 +88,7 @@ impl BarWidget for Cpu {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -89,17 +96,16 @@ impl BarWidget for Cpu {
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()
},
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) =

View File

@@ -1,60 +1,19 @@
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::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
/// 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
@@ -87,8 +46,6 @@ 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 {
@@ -102,14 +59,13 @@ impl DateFormat {
};
}
pub fn fmt_string(&self) -> String {
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(),
}
}
}
@@ -123,15 +79,9 @@ pub struct Date {
impl Date {
fn output(&mut self) -> String {
let formatted = chrono::Local::now()
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string();
// if custom modifiers are used, apply them
match &self.format {
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
_ => formatted,
}
.to_string()
}
}
@@ -140,6 +90,13 @@ impl BarWidget for Date {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -147,7 +104,7 @@ impl BarWidget for Date {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -159,22 +116,16 @@ impl BarWidget for Date {
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()
},
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false),
)
})
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.next()

View File

@@ -1,177 +0,0 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
use std::time::Instant;
use windows::Win32::Globalization::LCIDToLocaleName;
use windows::Win32::Globalization::LOCALE_ALLOW_NEUTRAL_NAMES;
use windows::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
const ERROR_TEXT: &str = "Error";
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KeyboardConfig {
/// Enable the Input widget
pub enable: bool,
/// Data refresh interval (default: 1 second)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<KeyboardConfig> for Keyboard {
fn from(value: KeyboardConfig) -> Self {
let data_refresh_interval = value
.data_refresh_interval
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
Self {
enable: value.enable,
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
lang_name: get_lang(),
}
}
}
pub struct Keyboard {
pub enable: bool,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
lang_name: String,
}
/// Retrieves the name of the active keyboard layout for the current foreground window.
///
/// This function determines the active keyboard layout by querying the system for the
/// foreground window's thread ID and its associated keyboard layout. It then attempts
/// to retrieve the locale name corresponding to the keyboard layout.
///
/// # Failure Cases
///
/// This function can fail in two distinct scenarios:
///
/// 1. **Failure to Retrieve the Locale Name**:
/// If the system fails to retrieve the locale name (e.g., due to an invalid or unsupported
/// language identifier), the function will return `Err(())`.
///
/// 2. **Invalid UTF-16 Characters in the Locale Name**:
/// If the retrieved locale name contains invalid UTF-16 sequences, the conversion to a Rust
/// `String` will fail, and the function will return `Err(())`.
///
/// # Returns
///
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
/// invalid UTF-16 characters during conversion.
fn get_active_keyboard_layout() -> Result<String, ()> {
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
// Extract the low word (language identifier) from the keyboard layout handle.
let lang_id = (lcid.0 as u32) & 0xFFFF;
let mut locale_name_buffer = [0; LOCALE_NAME_MAX_LENGTH as usize];
let char_count = unsafe {
LCIDToLocaleName(
lang_id,
Some(&mut locale_name_buffer),
LOCALE_ALLOW_NEUTRAL_NAMES,
)
};
match char_count {
0 => Err(()),
_ => String::from_utf16(&locale_name_buffer[..char_count as usize]).map_err(|_| ()),
}
}
/// Retrieves the name of the active keyboard layout or a fallback error message.
///
/// # Behavior
///
/// - **Success Case**:
/// If [`get_active_keyboard_layout`] succeeds, this function returns the retrieved keyboard
/// layout name as a `String`.
///
/// - **Failure Case**:
/// If [`get_active_keyboard_layout`] fails, this function returns the value of `ERROR_TEXT`
/// as a fallback message. This ensures that the function always returns a valid `String`,
/// even in error scenarios.
///
/// # Returns
///
/// A `String` representing either:
/// - The name of the active keyboard layout, or
/// - The fallback error message (`ERROR_TEXT`) if the layout name cannot be retrieved.
fn get_lang() -> String {
get_active_keyboard_layout()
.map(|l| l.trim_end_matches('\0').to_string())
.unwrap_or_else(|_| ERROR_TEXT.to_string())
}
impl Keyboard {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.last_updated = now;
self.lang_name = get_lang();
}
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("KB: {}", self.lang_name),
LabelPrefix::None | LabelPrefix::Icon => self.lang_name.clone(),
}
}
}
impl BarWidget for Keyboard {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::KEYBOARD.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
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()
},
);
config.apply_on_widget(true, ui, |ui| {
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
});
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,304 +0,0 @@
use crate::config::DisplayFormat;
use crate::komorebi::KomorebiLayoutConfig;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use eframe::egui::vec2;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Label;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Monocle,
Floating,
Paused,
Custom,
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) =
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
{
return Ok(KomorebiLayout::Default(default_layout));
}
// Handle other cases
match s.as_str() {
"Monocle" => Ok(KomorebiLayout::Monocle),
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
}
}
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Monocle => write!(f, "Monocle"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiLayout {
fn is_default(&mut self) -> bool {
matches!(self, KomorebiLayout::Default(_))
}
fn on_click(
&mut self,
show_options: &bool,
monitor_idx: usize,
workspace_idx: Option<usize>,
) -> bool {
if self.is_default() {
!show_options
} else {
self.on_click_option(monitor_idx, workspace_idx);
false
}
}
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
match self {
KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx {
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
monitor_idx,
ws_idx,
*option,
))
.is_err()
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
}
}
KomorebiLayout::Monocle => {
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
tracing::error!("could not send message to komorebi: ToggleMonocle");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
fn show_icon(&mut self, font_id: FontId, ctx: &Context, ui: &mut Ui) {
// paint custom icons for the layout
let size = Vec2::splat(font_id.size);
let (response, painter) = ui.allocate_painter(size, Sense::hover());
let color = ctx.style().visuals.selection.stroke.color;
let stroke = Stroke::new(1.0, color);
let mut rect = response.rect;
let rounding = Rounding::same(rect.width() * 0.1);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke);
match self {
KomorebiLayout::Default(layout) => match layout {
komorebi_client::DefaultLayout::BSP => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Columns => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Rows => {
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
}
komorebi_client::DefaultLayout::VerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
}
komorebi_client::DefaultLayout::RightMainVerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(r, 0.0), c], stroke);
}
komorebi_client::DefaultLayout::HorizontalStack => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c, c + vec2(0.0, r)], stroke);
}
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Grid => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
}
},
KomorebiLayout::Monocle => {}
KomorebiLayout::Floating => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.5);
rect_left.set_height(rect.height() * 0.5);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.1 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.35 + stroke.width,
rect.width() * 0.35 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_stroke(rect_right, rounding, stroke);
}
KomorebiLayout::Paused => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.25);
rect_left.set_height(rect.height() * 0.8);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.2 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.55 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_filled(rect_right, rounding, color);
}
KomorebiLayout::Custom => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke);
painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke);
}
}
}
pub fn show(
&mut self,
ctx: &Context,
ui: &mut Ui,
render_config: &mut RenderConfig,
layout_config: &KomorebiLayoutConfig,
workspace_idx: Option<usize>,
) {
let monitor_idx = render_config.monitor_idx;
let font_id = render_config.icon_font_id.clone();
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
if !self.is_default() {
show_options = false;
}
render_config.apply_on_widget(false, ui, |ui| {
let layout_frame = SelectableFrame::new(false)
.show(ui, |ui| {
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
self.show_icon(font_id.clone(), ctx, ui);
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
ui.add(Label::new(self.to_string()).selectable(false));
}
})
.on_hover_text(self.to_string());
if layout_frame.clicked() {
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
}
if show_options {
if let Some(workspace_idx) = workspace_idx {
Frame::none().show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::HorizontalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
for layout_option in &mut layout_options {
if SelectableFrame::new(self == layout_option)
.show(ui, |ui| layout_option.show_icon(font_id.clone(), ctx, ui))
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
}
}
});
RenderConfig::store_show_komorebi_layout_options(show_options);
}
}

View File

@@ -3,18 +3,14 @@ mod battery;
mod config;
mod cpu;
mod date;
mod keyboard;
mod komorebi;
mod komorebi_layout;
mod media;
mod memory;
mod network;
mod render;
mod selected_frame;
mod storage;
mod time;
mod ui;
mod update;
mod widget;
use crate::bar::Komobar;
@@ -22,26 +18,23 @@ 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;
use hotwatch::Hotwatch;
use image::RgbaImage;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::collections::HashMap;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId;
@@ -50,7 +43,6 @@ use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows_core::BOOL;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
@@ -58,10 +50,6 @@ 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()));
#[derive(Parser)]
#[clap(author, about, version)]
@@ -115,11 +103,6 @@ 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) }?;
@@ -237,43 +220,32 @@ fn main() -> color_eyre::Result<()> {
&SocketMessage::State,
)?)?;
let (usr_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),
};
let monitor_index = state
.monitor_usr_idx_map
.get(&usr_monitor_index)
.map_or(usr_monitor_index, |i| *i);
MONITOR_RIGHT.store(
state.monitors.elements()[monitor_index].size().right,
state.monitors.elements()[config.monitor.index].size().right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[monitor_index].size().top,
state.monitors.elements()[config.monitor.index].size().top,
Ordering::SeqCst,
);
MONITOR_LEFT.store(
state.monitors.elements()[monitor_index].size().left,
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().left,
Ordering::SeqCst,
);
MONITOR_INDEX.store(monitor_index, Ordering::SeqCst);
MONITOR_INDEX.store(config.monitor.index, Ordering::SeqCst);
match config.position {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[monitor_index].size().right as f32,
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
}),
})
@@ -281,14 +253,14 @@ fn main() -> color_eyre::Result<()> {
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
});
}
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[monitor_index].size().right as f32,
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
})
}
@@ -297,7 +269,7 @@ fn main() -> color_eyre::Result<()> {
let viewport_builder = ViewportBuilder::default()
.with_decorations(false)
.with_transparent(true)
.with_transparent(config.transparency_alpha.is_some())
.with_taskbar(false);
let native_options = eframe::NativeOptions {
@@ -305,9 +277,15 @@ fn main() -> color_eyre::Result<()> {
..Default::default()
};
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);
if let Some(rect) = &config.monitor.work_area_offset {
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
config.monitor.index,
*rect,
))?;
tracing::info!(
"work area offset applied to monitor: {}",
config.monitor.index
);
}
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
@@ -337,10 +315,13 @@ fn main() -> color_eyre::Result<()> {
tracing::info!("watching configuration file for changes");
let config_arc = Arc::new(config);
eframe::run_native(
"komorebi-bar",
native_options,
Box::new(|cc| {
let config_cl = config_arc.clone();
let ctx_repainter = cc.egui_ctx.clone();
std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(1));
@@ -361,10 +342,6 @@ fn main() -> color_eyre::Result<()> {
for client in listener.incoming() {
match client {
Ok(subscription) => {
match subscription.set_read_timeout(Some(Duration::from_secs(1))) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
}
let mut buffer = Vec::new();
let mut reader = BufReader::new(subscription);
@@ -383,12 +360,18 @@ fn main() -> color_eyre::Result<()> {
tracing::info!("reconnected to komorebi");
if let Err(error) = tx_gui.send(KomorebiEvent::Reconnect) {
tracing::error!("could not send komorebi reconnect event to gui thread: {error}")
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));
}
}
ctx_komorebi.request_repaint();
continue;
}
match String::from_utf8(buffer) {
@@ -399,7 +382,7 @@ fn main() -> color_eyre::Result<()> {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
if let Err(error) = tx_gui.send(notification) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}
@@ -424,7 +407,7 @@ fn main() -> color_eyre::Result<()> {
}
});
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))

View File

@@ -1,13 +1,14 @@
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
@@ -81,9 +82,16 @@ impl BarWidget for Media {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::HEADPHONES.to_string(),
config.icon_font_id.clone(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -91,28 +99,24 @@ impl BarWidget for Media {
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()
},
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
config.apply_on_widget(true, ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
)
})
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
self.toggle();

View File

@@ -1,12 +1,13 @@
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::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -29,18 +30,17 @@ pub struct MemoryConfig {
impl From<MemoryConfig> for Memory {
fn from(value: MemoryConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
let mut system =
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
system.refresh_memory();
Self {
enable: value.enable,
system: System::new_with_specifics(
RefreshKind::default().without_cpu().without_processes(),
),
data_refresh_interval,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
last_updated: Instant::now(),
}
}
}
@@ -77,6 +77,13 @@ impl BarWidget for Memory {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -84,7 +91,7 @@ impl BarWidget for Memory {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -92,17 +99,16 @@ impl BarWidget for Memory {
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()
},
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) =

View File

@@ -1,12 +1,13 @@
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::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use num_derive::FromPrimitive;
use schemars::JsonSchema;
@@ -26,8 +27,6 @@ pub struct NetworkConfig {
pub show_total_data_transmitted: bool,
/// Show network activity
pub show_network_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Characters to reserve for network activity data
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
@@ -38,40 +37,130 @@ pub struct NetworkConfig {
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
let mut last_state_data = vec![];
let mut last_state_transmitted = vec![];
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
let mut networks_network_activity = Networks::new_with_refreshed_list();
let mut default_interface = String::new();
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = interface.friendly_name {
default_interface.clone_from(&friendly_name);
if value.show_total_data_transmitted {
networks_total_data_transmitted.refresh();
for (interface_name, data) in &networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
last_state_data.push(match prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
if value.show_network_activity {
networks_network_activity.refresh();
for (interface_name, data) in &networks_network_activity {
if friendly_name.eq(interface_name) {
last_state_transmitted.push(match prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::IconAndText => format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
})
}
}
}
}
}
Self {
enable: value.enable,
show_total_activity: value.show_total_data_transmitted,
show_activity: value.show_network_activity,
show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
networks_total_data_transmitted,
networks_network_activity,
default_interface,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
show_total_data_transmitted: value.show_total_data_transmitted,
show_network_activity: value.show_network_activity,
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
last_state_total_data_transmitted: last_state_data,
last_state_network_activity: last_state_transmitted,
last_updated_total_data_transmitted: Instant::now(),
last_updated_network_activity: Instant::now(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
pub show_total_data_transmitted: bool,
pub show_network_activity: bool,
networks_total_data_transmitted: Networks,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>,
last_state_total_data_transmitted: Vec<String>,
last_state_network_activity: Vec<String>,
last_updated_total_data_transmitted: Instant,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
@@ -85,273 +174,214 @@ impl Network {
}
}
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
fn network_activity(&mut self) -> Vec<String> {
let mut outputs = self.last_state_network_activity.clone();
let now = Instant::now();
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
if self.show_network_activity
&& now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
activity.clear();
total_activity.clear();
outputs.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh(true);
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if self.show_activity {
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
if self.show_network_activity {
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
));
}
if self.show_total_activity {
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
Self::to_pretty_bytes(data.total_received(), 1),
Self::to_pretty_bytes(data.total_transmitted(), 1),
))
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::IconAndText => {
format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
)
}
})
}
}
}
}
}
self.last_state_activity.clone_from(&activity);
self.last_state_total_activity.clone_from(&total_activity);
self.last_state_network_activity.clone_from(&outputs);
self.last_updated_network_activity = now;
}
(activity, total_activity)
outputs
}
fn reading_to_label(
&self,
ctx: &Context,
reading: NetworkReading,
config: RenderConfig,
) -> Label {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"{: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"{: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("{} ", reading.received_text),
reading.transmitted_text,
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"UP: {: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s ", reading.received_text),
format!("\u{2211}UP: {}/s", reading.transmitted_text),
),
},
};
fn total_data_transmitted(&mut self) -> Vec<String> {
let mut outputs = self.last_state_total_data_transmitted.clone();
let now = Instant::now();
let icon_format = TextFormat::simple(
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
);
let text_format = TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
};
if self.show_total_data_transmitted
&& now.duration_since(self.last_updated_total_data_transmitted)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
// icon
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_total_data_transmitted {
self.networks_total_data_transmitted.refresh();
for (interface_name, data) in &self.networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
icon_format.font_id.clone(),
icon_format.color,
100.0,
);
}
// text
layout_job.append(
&text_down,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
// icon
layout_job.append(
&match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
0.0,
icon_format.clone(),
);
// text
layout_job.append(
&text_up,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
Label::new(layout_job).selectable(false)
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
self.last_state_total_data_transmitted.clone_from(&outputs);
self.last_updated_total_data_transmitted = now;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
outputs
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
// widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone();
if self.show_total_activity || self.show_activity {
let (activity, total_activity) = self.network_activity();
if self.show_total_activity {
for reading in total_activity {
render_config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone()));
});
}
}
if self.show_activity {
for reading in activity {
render_config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone()));
});
}
}
if self.show_total_data_transmitted {
for output in self.total_data_transmitted() {
config.apply_on_widget(true, ui, |ui| {
ui.add(Label::new(output).selectable(false));
});
}
if self.show_default_interface {
self.default_interface();
if !self.default_interface.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
render_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", "ncpa"]).spawn()
{
eprintln!("{}", error)
}
}
});
}
}
// widget spacing: pass on the config that was use for calling the apply_on_widget function
*config = render_config.clone();
}
}
}
#[derive(Clone)]
enum NetworkReadingFormat {
Speed = 0,
Total = 1,
}
if self.show_network_activity {
for output in self.network_activity() {
config.apply_on_widget(true, ui, |ui| {
ui.add(Label::new(output).selectable(false));
});
}
}
#[derive(Clone)]
struct NetworkReading {
pub format: NetworkReadingFormat,
pub received_text: String,
pub transmitted_text: String,
}
if self.enable {
self.default_interface();
impl NetworkReading {
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
NetworkReading {
format,
received_text: received,
transmitted_text: transmitted,
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error)
}
}
});
}
}
}
}
@@ -374,3 +404,22 @@ impl fmt::Display for DataUnit {
write!(f, "{:?}", self)
}
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}

View File

@@ -1,25 +1,16 @@
use crate::bar::Alignment;
use crate::config::KomobarConfig;
use crate::config::MonitorConfigOrIndex;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::InnerResponse;
use eframe::egui::Margin;
use eframe::egui::Rounding;
use eframe::egui::Shadow;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
@@ -34,10 +25,8 @@ pub enum Grouping {
Widget(GroupingConfig),
}
#[derive(Clone)]
#[derive(Copy, Clone)]
pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize,
/// Spacing between widgets
pub spacing: f32,
/// Sets how widgets are grouped
@@ -50,102 +39,49 @@ pub struct RenderConfig {
pub more_inner_margin: bool,
/// Set to true after the first time the apply_on_widget was called on an alignment
pub applied_on_widget: bool,
/// FontId for text
pub text_font_id: FontId,
/// FontId for icon (based on scaling the text font id)
pub icon_font_id: FontId,
}
pub trait RenderExt {
fn new_renderconfig(
&self,
ctx: &Context,
background_color: Color32,
icon_scale: Option<f32>,
) -> RenderConfig;
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig;
}
impl RenderExt for &KomobarConfig {
fn new_renderconfig(
&self,
ctx: &Context,
background_color: Color32,
icon_scale: Option<f32>,
) -> RenderConfig {
let text_font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
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,
};
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
RenderConfig {
monitor_idx,
spacing: self.widget_spacing.unwrap_or(10.0),
grouping: self.grouping.unwrap_or(Grouping::None),
background_color,
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id,
icon_font_id,
}
}
}
impl RenderConfig {
pub fn load_show_komorebi_layout_options() -> bool {
SHOW_KOMOREBI_LAYOUT_OPTIONS.load(Ordering::SeqCst) != 0
}
pub fn store_show_komorebi_layout_options(show: bool) {
SHOW_KOMOREBI_LAYOUT_OPTIONS.store(show as usize, Ordering::SeqCst);
}
pub fn new() -> Self {
Self {
monitor_idx: 0,
spacing: 0.0,
grouping: Grouping::None,
background_color: Color32::BLACK,
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id: FontId::default(),
icon_font_id: FontId::default(),
}
}
pub fn change_frame_on_bar(
pub fn apply_on_bar<R>(
&mut self,
frame: Frame,
ui_style: &Arc<eframe::egui::Style>,
) -> Frame {
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.alignment = None;
if let Grouping::Bar(config) = self.grouping {
return self.define_group_frame(
//TODO: this outer margin can be a config
Some(Margin {
left: 10.0,
right: 10.0,
top: 6.0,
bottom: 6.0,
}),
config,
ui_style,
);
return self.define_group(None, config, ui, add_contents);
}
frame
Self::fallback_group(ui, add_contents)
}
pub fn apply_on_alignment<R>(
@@ -207,26 +143,16 @@ impl RenderConfig {
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.define_group_frame(outer_margin, config, ui.style())
.show(ui, add_contents)
}
pub fn define_group_frame(
&mut self,
outer_margin: Option<Margin>,
config: GroupingConfig,
ui_style: &Arc<eframe::egui::Style>,
) -> Frame {
Frame::group(ui_style)
Frame::group(ui.style_mut())
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
.inner_margin(match self.more_inner_margin {
true => Margin::symmetric(6.0, 1.0),
false => Margin::symmetric(1.0, 1.0),
true => Margin::symmetric(8.0, 3.0),
false => Margin::symmetric(3.0, 3.0),
})
.stroke(ui_style.visuals.widgets.noninteractive.bg_stroke)
.stroke(ui.style().visuals.widgets.noninteractive.bg_stroke)
.rounding(match config.rounding {
Some(rounding) => rounding.into(),
None => ui_style.visuals.widgets.noninteractive.rounding,
None => ui.style().visuals.widgets.noninteractive.rounding,
})
.fill(
self.background_color
@@ -236,60 +162,16 @@ impl RenderConfig {
Some(style) => match style {
// new styles can be added if needed here
GroupingStyle::Default => Shadow::NONE,
GroupingStyle::DefaultWithShadowB4O1S3 => Shadow {
GroupingStyle::DefaultWithShadow => Shadow {
blur: 4.0,
offset: Vec2::new(1.0, 1.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithShadowB4O0S3 => Shadow {
blur: 4.0,
offset: Vec2::new(0.0, 0.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithShadowB0O1S3 => Shadow {
blur: 0.0,
offset: Vec2::new(1.0, 1.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB3O1S2 => Shadow {
blur: 3.0,
offset: Vec2::new(1.0, 1.0),
spread: 2.0,
color: ui_style
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB3O0S2 => Shadow {
blur: 3.0,
offset: Vec2::new(0.0, 0.0),
spread: 2.0,
color: ui_style
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB0O1S2 => Shadow {
blur: 0.0,
offset: Vec2::new(1.0, 1.0),
spread: 2.0,
color: ui_style
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
},
None => Shadow::NONE,
})
.show(ui, add_contents)
}
fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin {
@@ -308,7 +190,6 @@ impl RenderConfig {
left: match self.alignment {
Some(align) => match align {
Alignment::Left => spacing,
Alignment::Center => spacing,
Alignment::Right => 0.0,
},
None => 0.0,
@@ -316,7 +197,6 @@ impl RenderConfig {
right: match self.alignment {
Some(align) => match align {
Alignment::Left => 0.0,
Alignment::Center => 0.0,
Alignment::Right => spacing,
},
None => 0.0,
@@ -341,20 +221,9 @@ pub struct GroupingConfig {
pub enum GroupingStyle {
#[serde(alias = "CtByte")]
Default,
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
/// A black shadow is added under the default group
#[serde(alias = "CtByteWithShadow")]
#[serde(alias = "DefaultWithShadow")]
DefaultWithShadowB4O1S3,
/// A shadow is added under the default group. (blur: 4, offset: x-0 y-0, spread: 3)
DefaultWithShadowB4O0S3,
/// A shadow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 3)
DefaultWithShadowB0O1S3,
/// A glow is added under the default group. (blur: 3, offset: x-1 y-1, spread: 2)
DefaultWithGlowB3O1S2,
/// A glow is added under the default group. (blur: 3, offset: x-0 y-0, spread: 2)
DefaultWithGlowB3O0S2,
/// A glow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 2)
DefaultWithGlowB0O1S2,
DefaultWithShadow,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]

View File

@@ -1,56 +0,0 @@
use eframe::egui::CursorIcon;
use eframe::egui::Frame;
use eframe::egui::Margin;
use eframe::egui::Response;
use eframe::egui::Sense;
use eframe::egui::Ui;
/// Same as SelectableLabel, but supports all content
pub struct SelectableFrame {
selected: bool,
}
impl SelectableFrame {
pub fn new(selected: bool) -> Self {
Self { selected }
}
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
let Self { selected } = self;
Frame::none()
.show(ui, |ui| {
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
if ui.is_rect_visible(response.rect) {
let inner_margin = Margin::symmetric(
ui.style().spacing.button_padding.x,
ui.style().spacing.button_padding.y,
);
if selected
|| response.hovered()
|| response.highlighted()
|| response.has_focus()
{
let visuals = ui.style().interact_selectable(&response, selected);
Frame::none()
.stroke(visuals.bg_stroke)
.rounding(visuals.rounding)
.fill(visuals.bg_fill)
.inner_margin(inner_margin)
.show(ui, add_contents);
} else {
Frame::none()
.inner_margin(inner_margin)
.show(ui, add_contents);
}
}
response
})
.inner
.on_hover_cursor(CursorIcon::PointingHand)
}
}

View File

@@ -1,12 +1,13 @@
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::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -50,7 +51,7 @@ impl Storage {
fn output(&mut self) -> Vec<String> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh(true);
self.disks.refresh();
self.last_updated = now;
}
@@ -80,6 +81,13 @@ impl Storage {
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
for output in self.output() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
@@ -88,7 +96,7 @@ impl BarWidget for Storage {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -96,17 +104,16 @@ impl BarWidget for Storage {
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()
},
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe")

View File

@@ -1,18 +1,14 @@
use crate::bar::Alignment;
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::FontId;
use eframe::egui::Label;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -41,16 +37,8 @@ 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),
}
@@ -58,12 +46,8 @@ pub enum TimeFormat {
impl TimeFormat {
pub fn toggle(&mut self) {
match self {
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,
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
_ => {}
};
}
@@ -71,11 +55,7 @@ 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(),
}
}
@@ -93,169 +73,6 @@ 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);
}
}
}
@@ -264,8 +81,12 @@ 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 font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
@@ -274,7 +95,7 @@ impl BarWidget for Time {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -283,83 +104,19 @@ impl BarWidget for Time {
output.insert_str(0, "TIME: ");
}
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()
},
);
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
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| {
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));
}
})
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.toggle()

View File

@@ -1,158 +0,0 @@
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::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct UpdateConfig {
/// Enable the Update widget
pub enable: bool,
/// Data refresh interval (default: 12 hours)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<UpdateConfig> for Update {
fn from(value: UpdateConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(12);
let mut latest_version = String::new();
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebi-bar-version-checker")
.send()
{
#[derive(Deserialize)]
struct Release {
tag_name: String,
}
if let Ok(release) =
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
{
let trimmed = release.tag_name.trim_start_matches("v");
latest_version = trimmed.to_string();
}
}
Self {
enable: value.enable,
data_refresh_interval,
installed_version: env!("CARGO_PKG_VERSION").to_string(),
latest_version,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
pub struct Update {
pub enable: bool,
data_refresh_interval: u64,
installed_version: String,
latest_version: String,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Update {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated)
> Duration::from_secs((self.data_refresh_interval * 60) * 60)
{
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebi-bar-version-checker")
.send()
{
#[derive(Deserialize)]
struct Release {
tag_name: String,
}
if let Ok(release) =
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
{
let trimmed = release.tag_name.trim_start_matches("v");
self.latest_version = trimmed.to_string();
}
}
self.last_updated = now;
}
if self.latest_version > self.installed_version {
format!("Update available! v{}", self.latest_version)
} else {
String::new()
}
}
}
impl BarWidget for Update {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ROCKET_LAUNCH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
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()
},
);
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("explorer.exe")
.args([format!(
"https://github.com/LGUG2Z/komorebi/releases/v{}",
self.latest_version
)])
.spawn()
{
eprintln!("{}", error)
}
}
});
}
}
}
}

View File

@@ -4,8 +4,6 @@ use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::keyboard::Keyboard;
use crate::keyboard::KeyboardConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
@@ -19,8 +17,6 @@ use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use crate::update::Update;
use crate::update::UpdateConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
@@ -36,14 +32,12 @@ pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
Date(DateConfig),
Keyboard(KeyboardConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
Memory(MemoryConfig),
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
Update(UpdateConfig),
}
impl WidgetConfig {
@@ -52,38 +46,12 @@ impl WidgetConfig {
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
}
}
pub fn enabled(&self) -> bool {
match self {
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Keyboard(config) => config.enable,
WidgetConfig::Komorebi(config) => {
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()
.is_some_and(|w| w.enable)
}
WidgetConfig::Media(config) => config.enable,
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
WidgetConfig::Update(config) => config.enable,
}
}
}

View File

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

View File

@@ -1,16 +1,10 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::animation::PerAnimationPrefixConfig;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::config_generation::IdWithIdentifier;
pub use komorebi::config_generation::IdWithIdentifierAndComment;
pub use komorebi::config_generation::MatchingRule;
pub use komorebi::config_generation::MatchingStrategy;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
@@ -20,10 +14,6 @@ pub use komorebi::core::Arrangement;
pub use komorebi::core::Axis;
pub use komorebi::core::BorderImplementation;
pub use komorebi::core::BorderStyle;
pub use komorebi::core::Column;
pub use komorebi::core::ColumnSplit;
pub use komorebi::core::ColumnSplitWithCapacity;
pub use komorebi::core::ColumnWidth;
pub use komorebi::core::CustomLayout;
pub use komorebi::core::CycleDirection;
pub use komorebi::core::DefaultLayout;
@@ -34,7 +24,6 @@ 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;
@@ -43,31 +32,21 @@ 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;
pub use komorebi::workspace::Workspace;
pub use komorebi::workspace::WorkspaceLayer;
pub use komorebi::AnimationsConfig;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi::WindowContainerBehaviour;
pub use komorebi::WindowsApi;
pub use komorebi::WorkspaceConfig;
use komorebi::DATA_DIR;
@@ -75,7 +54,6 @@ use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::Shutdown;
use std::time::Duration;
pub use uds_windows::UnixListener;
use uds_windows::UnixStream;
@@ -84,30 +62,13 @@ const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
if let Ok(m_str) = serde_json::to_string(&m) {
s.push_str(&m_str);
s.push('\n');
}
s
});
stream.write_all(msgs.as_bytes())
}
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_read_timeout(Some(Duration::from_secs(1)))?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
stream.shutdown(Shutdown::Write)?;

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-gui"
version = "0.1.35"
version = "0.1.31"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -10,7 +10,6 @@ komorebi-client = { path = "../komorebi-client" }
eframe = { workspace = true }
egui_extras = { workspace = true }
random_word = { version = "0.4", features = ["en"] }
random_word = { version = "0.4.3", features = ["en"] }
serde_json = { workspace = true }
windows-core = { workspace = true }
windows = { workspace = true }

View File

@@ -215,7 +215,7 @@ impl KomorebiGui {
extern "system" fn enum_window(
hwnd: windows::Win32::Foundation::HWND,
lparam: windows::Win32::Foundation::LPARAM,
) -> windows_core::BOOL {
) -> windows::Win32::Foundation::BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let window = Window::from(hwnd.0 as isize);

View File

@@ -1,14 +1,14 @@
[package]
name = "komorebi-themes"
version = "0.1.35"
version = "0.1.31"
edition = "2021"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "911079d" }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f85cc3c", default-features = false, features = ["egui30"] }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "c11fbe2a3a4681485c5065b899a4c4d85fad3b04" }
#catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f579847bf2f552b144361d5a78ed8cf360b55cbb" }
catppuccin-egui = { version = "5", default-features = false, features = ["egui29"] }
eframe = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_variant = "0.1"
strum = "0.26"
strum = "0.26"

View File

@@ -4,7 +4,6 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::IntoEnumIterator;
pub use base16_egui_themes::Base16;
@@ -12,7 +11,7 @@ pub use catppuccin_egui;
pub use eframe::egui::Color32;
use serde_variant::to_variant_name;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum Theme {
/// A theme from catppuccin-egui
@@ -49,7 +48,7 @@ impl Theme {
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub enum Base16Value {
Base00,
Base01,
@@ -93,7 +92,7 @@ impl Base16Value {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum Catppuccin {
Frappe,
Latte,
@@ -118,7 +117,7 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub enum CatppuccinValue {
Rosewater,
Flamingo,

View File

@@ -1,7 +1,8 @@
[package]
name = "komorebi"
version = "0.1.35"
version = "0.1.31"
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
@@ -25,7 +26,7 @@ lazy_static = { workspace = true }
miow = "0.6"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.10"
os_info = "3.8"
parking_lot = "0.12"
paste = { workspace = true }
regex = "1"
@@ -41,20 +42,17 @@ tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
uds_windows = { workspace = true }
which = { workspace = true }
widestring = "1"
win32-display-data = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }
windows-numerics = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.55"
winreg = "0.52"
[build-dependencies]
shadow-rs = { workspace = true }
[dev-dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
[features]
deadlock_detection = ["parking_lot/deadlock_detection"]

View File

@@ -1,5 +1,3 @@
use shadow_rs::ShadowBuilder;
fn main() {
ShadowBuilder::builder().build().unwrap();
shadow_rs::new().unwrap();
}

View File

@@ -1,6 +1,22 @@
use crate::core::AnimationStyle;
use crate::core::Rect;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::f64::consts::PI;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_MANAGER;
use crate::ANIMATION_STYLE;
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
pub trait Ease {
fn evaluate(t: f64) -> f64;
@@ -354,8 +370,9 @@ impl Ease for EaseInOutBounce {
}
}
}
fn apply_ease_func(t: f64) -> f64 {
let style = *ANIMATION_STYLE.lock();
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
match style {
AnimationStyle::Linear => Linear::evaluate(t),
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
@@ -389,3 +406,112 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
}
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct Animation {
pub hwnd: isize,
}
impl Animation {
pub fn new(hwnd: isize) -> Self {
Self { hwnd }
}
/// Returns true if the animation needs to continue
pub fn cancel(&mut self) -> bool {
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
return true;
}
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
let max_duration = Duration::from_secs(1);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(self.hwnd);
}
std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
));
}
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
latest_cancel_idx == cancel_idx
}
#[allow(clippy::cast_possible_truncation)]
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
let time = apply_ease_func(t);
f64::from(end - start)
.mul_add(time, f64::from(start))
.round() as i32
}
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
Rect {
left: Self::lerp(start_rect.left, end_rect.left, t),
top: Self::lerp(start_rect.top, end_rect.top, t),
right: Self::lerp(start_rect.right, end_rect.right, t),
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
}
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
&mut self,
duration: Duration,
mut render_callback: impl FnMut(f64) -> Result<()>,
) -> Result<()> {
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
let should_animate = self.cancel();
if !should_animate {
return Ok(());
}
}
ANIMATION_MANAGER.lock().start(self.hwnd);
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
// cancel animation
ANIMATION_MANAGER.lock().cancel(self.hwnd);
return Ok(());
}
let frame_start = Instant::now();
// calculate progress
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_callback(progress).ok();
// sleep until next frame
let frame_time_elapsed = frame_start.elapsed();
if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}
ANIMATION_MANAGER.lock().end(self.hwnd);
// limit progress to 1.0 if animation took longer
if progress > 1.0 {
progress = 1.0;
}
// process animation for 1.0 to set target position
render_callback(progress)
}
}

View File

@@ -1,115 +0,0 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use super::prefix::AnimationPrefix;
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}
#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<String, AnimationState>,
}
impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}
impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}
pub fn is_cancelled(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.pending_cancel_count > 0
} else {
false
}
}
pub fn in_progress(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.in_progress
} else {
false
}
}
pub fn init_cancel(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;
// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn latest_cancel_idx(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn end_cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count -= 1;
}
}
pub fn cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;
}
}
pub fn start(&mut self, animation_key: &str) {
if let Entry::Vacant(e) = self.animations.entry(animation_key.to_string()) {
e.insert(AnimationState {
in_progress: true,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});
return;
}
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = true;
}
}
pub fn end(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;
if animation_state.pending_cancel_count == 0 {
self.animations.remove(animation_key);
}
}
}
pub fn count_in_progress(&self, animation_key_prefix: AnimationPrefix) -> usize {
self.animations
.keys()
.filter(|key| key.starts_with(animation_key_prefix.to_string().as_str()))
.count()
}
pub fn count(&self) -> usize {
self.animations.len()
}
}

View File

@@ -1,122 +0,0 @@
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use super::RenderDispatcher;
use super::ANIMATION_DURATION_GLOBAL;
use super::ANIMATION_FPS;
use super::ANIMATION_MANAGER;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct AnimationEngine;
impl AnimationEngine {
pub fn wait_for_all_animations() {
let max_duration = Duration::from_secs(20);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().count() > 0 {
if spent_duration.elapsed() >= max_duration {
break;
}
std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst),
));
}
}
/// Returns true if the animation needs to continue
pub fn cancel(animation_key: &str) -> bool {
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(animation_key);
let max_duration = Duration::from_secs(5);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().in_progress(animation_key) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(animation_key);
}
std::thread::sleep(Duration::from_millis(250 / 2));
}
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(animation_key);
ANIMATION_MANAGER.lock().end_cancel(animation_key);
latest_cancel_idx == cancel_idx
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
render_dispatcher: (impl RenderDispatcher + Send + 'static),
duration: Duration,
) -> Result<()> {
std::thread::spawn(move || {
let animation_key = render_dispatcher.get_animation_key();
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
let should_animate = Self::cancel(animation_key.as_str());
if !should_animate {
return Ok(());
}
}
render_dispatcher.pre_render()?;
ANIMATION_MANAGER.lock().start(animation_key.as_str());
let target_frame_time =
Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER
.lock()
.is_cancelled(animation_key.as_str())
{
// cancel animation
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
return Ok(());
}
let frame_start = Instant::now();
// calculate progress
progress =
animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_dispatcher.render(progress).ok();
// sleep until next frame
let frame_time_elapsed = frame_start.elapsed();
if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}
ANIMATION_MANAGER.lock().end(animation_key.as_str());
// limit progress to 1.0 if animation took longer
if progress != 1.0 {
progress = 1.0;
// process animation for 1.0 to set target position
render_dispatcher.render(progress).ok();
}
render_dispatcher.post_render()
});
Ok(())
}
}

View File

@@ -1,42 +0,0 @@
use crate::core::Rect;
use crate::AnimationStyle;
use super::style::apply_ease_func;
pub trait Lerp<T = Self> {
fn lerp(self, end: T, time: f64, style: AnimationStyle) -> T;
}
impl Lerp for i32 {
#[allow(clippy::cast_possible_truncation)]
fn lerp(self, end: i32, time: f64, style: AnimationStyle) -> i32 {
let time = apply_ease_func(time, style);
f64::from(end - self).mul_add(time, f64::from(self)).round() as i32
}
}
impl Lerp for f64 {
fn lerp(self, end: f64, time: f64, style: AnimationStyle) -> f64 {
let time = apply_ease_func(time, style);
(end - self).mul_add(time, self)
}
}
impl Lerp for u8 {
fn lerp(self, end: u8, time: f64, style: AnimationStyle) -> u8 {
(self as f64).lerp(end as f64, time, style) as u8
}
}
impl Lerp for Rect {
fn lerp(self, end: Rect, time: f64, style: AnimationStyle) -> Rect {
Rect {
left: self.left.lerp(end.left, time, style),
top: self.top.lerp(end.top, time, style),
right: self.right.lerp(end.right, time, style),
bottom: self.bottom.lerp(end.bottom, time, style),
}
}
}

View File

@@ -1,54 +0,0 @@
use crate::animation::animation_manager::AnimationManager;
use crate::core::animation::AnimationStyle;
use lazy_static::lazy_static;
use prefix::AnimationPrefix;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use parking_lot::Mutex;
pub use engine::AnimationEngine;
pub mod animation_manager;
pub mod engine;
pub mod lerp;
pub mod prefix;
pub mod render_dispatcher;
pub use render_dispatcher::RenderDispatcher;
pub mod style;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum PerAnimationPrefixConfig<T> {
Prefix(HashMap<AnimationPrefix, T>),
Global(T),
}
pub const DEFAULT_ANIMATION_ENABLED: bool = false;
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
lazy_static! {
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
pub static ref ANIMATION_STYLE_GLOBAL: Arc<Mutex<AnimationStyle>> =
Arc::new(Mutex::new(DEFAULT_ANIMATION_STYLE));
pub static ref ANIMATION_ENABLED_GLOBAL: Arc<AtomicBool> =
Arc::new(AtomicBool::new(DEFAULT_ANIMATION_ENABLED));
pub static ref ANIMATION_DURATION_GLOBAL: Arc<AtomicU64> =
Arc::new(AtomicU64::new(DEFAULT_ANIMATION_DURATION));
pub static ref ANIMATION_STYLE_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, AnimationStyle>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_ENABLED_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, bool>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_DURATION_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, u64>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);

View File

@@ -1,31 +0,0 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(
Copy,
Clone,
Debug,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum AnimationPrefix {
Movement,
Transparency,
}
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
format!("{}:{}", prefix, key)
}

View File

@@ -1,8 +0,0 @@
use color_eyre::Result;
pub trait RenderDispatcher {
fn get_animation_key(&self) -> String;
fn pre_render(&self) -> Result<()>;
fn render(&self, delta: f64) -> Result<()>;
fn post_render(&self) -> Result<()>;
}

View File

@@ -0,0 +1,108 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}
#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<isize, AnimationState>,
}
impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}
impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}
pub fn is_cancelled(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.pending_cancel_count > 0
} else {
false
}
}
pub fn in_progress(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.in_progress
} else {
false
}
}
pub fn init_cancel(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;
// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn latest_cancel_idx(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn end_cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count -= 1;
}
}
pub fn cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
}
}
pub fn start(&mut self, hwnd: isize) {
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
e.insert(AnimationState {
in_progress: true,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
return;
}
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = true;
}
}
pub fn end(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
if animation_state.pending_cancel_count == 0 {
self.animations.remove(&hwnd);
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
}
}
}
}

View File

@@ -1,23 +1,22 @@
use crate::border_manager::window_kind_colour;
use crate::border_manager::RenderTarget;
use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUS_STATE;
use crate::border_manager::RENDER_TARGETS;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::core::BorderStyle;
use crate::core::Rect;
use crate::windows_api;
use crate::WindowsApi;
use crate::WINDOWS_11;
use color_eyre::eyre::anyhow;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::LazyLock;
use std::sync::OnceLock;
use windows::Foundation::Numerics::Matrix3x2;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::FALSE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
@@ -31,7 +30,6 @@ use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
@@ -45,50 +43,30 @@ use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreateRectRgn;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SIZE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows_core::BOOL;
use windows_core::PCWSTR;
use windows_numerics::Matrix3x2;
pub struct RenderFactory(ID2D1Factory);
unsafe impl Sync for RenderFactory {}
unsafe impl Send for RenderFactory {}
impl Deref for RenderFactory {
type Target = ID2D1Factory;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[allow(clippy::expect_used)]
static RENDER_FACTORY: LazyLock<RenderFactory> = unsafe {
static RENDER_FACTORY: LazyLock<ID2D1Factory> = unsafe {
LazyLock::new(|| {
RenderFactory(
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
.expect("creating RENDER_FACTORY failed"),
)
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
.expect("creating RENDER_FACTORY failed")
})
};
@@ -111,36 +89,14 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Border {
pub hwnd: isize,
pub render_target: OnceLock<RenderTarget>,
pub tracking_hwnd: isize,
pub window_rect: Rect,
pub window_kind: WindowKind,
pub style: BorderStyle,
pub width: i32,
pub offset: i32,
pub brush_properties: D2D1_BRUSH_PROPERTIES,
pub rounded_rect: D2D1_ROUNDED_RECT,
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
}
impl From<isize> for Border {
fn from(value: isize) -> Self {
Self {
hwnd: value,
render_target: OnceLock::new(),
tracking_hwnd: 0,
window_rect: Rect::default(),
window_kind: WindowKind::Unfocused,
style: STYLE.load(),
width: BORDER_WIDTH.load(Ordering::Relaxed),
offset: BORDER_OFFSET.load(Ordering::Relaxed),
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
rounded_rect: D2D1_ROUNDED_RECT::default(),
brushes: HashMap::new(),
}
Self { hwnd: value }
}
}
@@ -149,7 +105,7 @@ impl Border {
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result<Self> {
pub fn create(id: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
@@ -165,36 +121,18 @@ impl Border {
let _ = WindowsApi::register_class_w(&window_class);
let (border_sender, border_receiver) = mpsc::channel();
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let instance = h_module.0 as isize;
std::thread::spawn(move || -> color_eyre::Result<()> {
let mut border = Self {
hwnd: 0,
render_target: OnceLock::new(),
tracking_hwnd,
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
window_kind: WindowKind::Unfocused,
style: STYLE.load(),
width: BORDER_WIDTH.load(Ordering::Relaxed),
offset: BORDER_OFFSET.load(Ordering::Relaxed),
brush_properties: Default::default(),
rounded_rect: Default::default(),
brushes: HashMap::new(),
};
let border_pointer = std::ptr::addr_of!(border);
let hwnd =
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
border.hwnd = hwnd;
border_sender.send(border_pointer as isize)?;
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
hwnd_sender.send(hwnd)?;
let mut msg: MSG = MSG::default();
loop {
unsafe {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
tracing::debug!("border window event processing thread shutdown");
break;
};
@@ -207,8 +145,8 @@ impl Border {
Ok(())
});
let border_ref = border_receiver.recv()?;
let border = unsafe { &mut *(border_ref as *mut Border) };
let hwnd = hwnd_receiver.recv()?;
let border = Self { hwnd };
// I have literally no idea, apparently this is to get rid of the black pixels
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
@@ -229,7 +167,7 @@ impl Border {
}
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
hwnd: HWND(windows_api::as_ptr!(hwnd)),
pixelSize: Default::default(),
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
};
@@ -250,51 +188,10 @@ impl Border {
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
} {
Ok(render_target) => unsafe {
border.brush_properties = *BRUSH_PROPERTIES.deref();
for window_kind in [
WindowKind::Single,
WindowKind::Stack,
WindowKind::Monocle,
WindowKind::Unfocused,
WindowKind::Floating,
] {
let color = window_kind_colour(window_kind);
let color = D2D1_COLOR_F {
r: ((color & 0xFF) as f32) / 255.0,
g: (((color >> 8) & 0xFF) as f32) / 255.0,
b: (((color >> 16) & 0xFF) as f32) / 255.0,
a: 1.0,
};
if let Ok(brush) =
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
{
border.brushes.insert(window_kind, brush);
}
}
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
if border
.render_target
.set(RenderTarget(render_target.clone()))
.is_err()
{
return Err(anyhow!("could not store border render target"));
}
border.rounded_rect = {
let radius = 8.0 + border.width as f32 / 2.0;
D2D1_ROUNDED_RECT {
rect: Default::default(),
radiusX: radius,
radiusY: radius,
}
};
let mut render_targets = RENDER_TARGETS.lock();
render_targets.insert(border.hwnd, RenderTarget(render_target));
Ok(border.clone())
render_targets.insert(hwnd, render_target);
Ok(border)
},
Err(error) => Err(error.into()),
}
@@ -306,19 +203,29 @@ impl Border {
WindowsApi::close_window(self.hwnd)
}
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
pub fn update(&self, rect: &Rect, should_invalidate: bool) -> color_eyre::Result<()> {
// Make adjustments to the border
let mut rect = *rect;
rect.add_margin(self.width);
rect.add_padding(-self.offset);
rect.add_margin(BORDER_WIDTH.load(Ordering::Relaxed));
rect.add_padding(-BORDER_OFFSET.load(Ordering::Relaxed));
WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?;
// Update the position of the border if required
// This effectively handles WM_MOVE
// Also if I remove this no borders render at all lol
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
}
// Invalidate the rect to trigger the callback to update colours etc.
if should_invalidate {
self.invalidate();
}
Ok(())
}
// this triggers WM_PAINT in the callback below
pub fn invalidate(&self) {
let _ = unsafe { InvalidateRect(Option::from(self.hwnd()), None, false) };
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
}
pub extern "system" fn callback(
@@ -329,168 +236,50 @@ impl Border {
) -> LRESULT {
unsafe {
match message {
WM_CREATE => {
let mut border_pointer: *mut Border =
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _;
border_pointer = (*create_struct).lpCreateParams as *mut _;
SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _);
}
LRESULT(0)
}
EVENT_OBJECT_DESTROY => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
// we don't actually want to destroy the window here, just hide it for quicker
// visual feedback to the user; the actual destruction will be handled by the
// core border manager loop
WindowsApi::hide_window(window.0 as isize);
LRESULT(0)
}
EVENT_OBJECT_LOCATIONCHANGE => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
let reference_hwnd = lparam.0;
let old_rect = (*border_pointer).window_rect;
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
(*border_pointer).window_rect = rect;
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
tracing::error!("failed to update border position {error}");
}
if !rect.is_same_size_as(&old_rect) {
if let Some(render_target) = (*border_pointer).render_target.get() {
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
let _ = render_target.EndDraw(None, None);
}
}
}
LRESULT(0)
}
WM_PAINT => {
WM_SIZE | WM_PAINT => {
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
let border_pointer: *mut Border =
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
let render_targets = RENDER_TARGETS.lock();
if let Some(render_target) = render_targets.get(&(window.0 as isize)) {
let pixel_size = D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
};
if border_pointer.is_null() {
return LRESULT(0);
}
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
let reference_hwnd = (*border_pointer).tracking_hwnd;
// Update position to update the ZOrder
let border_window_rect = (*border_pointer).window_rect;
tracing::trace!("updating border position");
if let Err(error) =
(*border_pointer).set_position(&border_window_rect, reference_hwnd)
{
tracing::error!("failed to update border position {error}");
}
if let Some(render_target) = (*border_pointer).render_target.get() {
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
let rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
let _ = render_target.Resize(&pixel_size);
// Get window kind and color
(*border_pointer).window_kind = FOCUS_STATE
let window_kind = FOCUS_STATE
.lock()
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused);
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
let color = window_kind_colour(window_kind);
let color = D2D1_COLOR_F {
r: ((color & 0xFF) as f32) / 255.0,
g: (((color >> 8) & 0xFF) as f32) / 255.0,
b: (((color >> 16) & 0xFF) as f32) / 255.0,
a: 1.0,
};
if let Ok(brush) = render_target
.CreateSolidColorBrush(&color, Some(BRUSH_PROPERTIES.deref()))
{
render_target.BeginDraw();
render_target.Clear(None);
(*border_pointer).style = STYLE.load();
// Calculate border radius based on style
let style = match (*border_pointer).style {
let style = match STYLE.load() {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
@@ -504,17 +293,31 @@ impl Border {
match style {
BorderStyle::Rounded => {
let radius = 8.0 + border_width as f32 / 2.0;
let rounded_rect = D2D1_ROUNDED_RECT {
rect,
radiusX: radius,
radiusY: radius,
};
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
&rounded_rect,
&brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
let rect = D2D_RECT_F {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
};
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
&rect,
&brush,
border_width as f32,
None,
);
@@ -523,14 +326,17 @@ impl Border {
}
let _ = render_target.EndDraw(None, None);
// If we don't do this we'll get spammed with WM_PAINT according to Raymond Chen
// https://stackoverflow.com/questions/41783234/why-does-my-call-to-d2d1rendertargetdrawtext-result-in-a-wm-paint-being-se#comment70756781_41783234
let _ = BeginPaint(window, &mut PAINTSTRUCT::default());
let _ = EndPaint(window, &PAINTSTRUCT::default());
}
}
}
let _ = ValidateRect(Option::from(window), None);
LRESULT(0)
}
WM_DESTROY => {
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
PostQuitMessage(0);
LRESULT(0)
}

View File

@@ -11,7 +11,7 @@ use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
pub use border::Border;
use border::Border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
@@ -23,22 +23,25 @@ use serde::Deserialize;
use serde::Serialize;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use strum::Display;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
use windows::Win32::System::Threading::GetCurrentThread;
use windows::Win32::System::Threading::SetThreadPriority;
use windows::Win32::System::Threading::THREAD_PRIORITY_TIME_CRITICAL;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
lazy_static! {
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
AtomicCell::new(BorderImplementation::Komorebi);
@@ -56,21 +59,9 @@ lazy_static! {
lazy_static! {
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
static ref RENDER_TARGETS: Mutex<HashMap<isize, RenderTarget>> = Mutex::new(HashMap::new());
}
#[derive(Debug, Clone)]
pub struct RenderTarget(pub ID2D1HwndRenderTarget);
unsafe impl Send for RenderTarget {}
impl Deref for RenderTarget {
type Target = ID2D1HwndRenderTarget;
fn deref(&self) -> &Self::Target {
&self.0
}
static ref RENDER_TARGETS: Mutex<HashMap<isize, ID2D1HwndRenderTarget>> =
Mutex::new(HashMap::new());
}
pub struct Notification(pub Option<isize>);
@@ -89,10 +80,6 @@ fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn window_border(hwnd: isize) -> Option<Border> {
WINDOWS_BORDERS.lock().get(&hwnd).cloned()
}
pub fn send_notification(hwnd: Option<isize>) {
if event_tx().try_send(Notification(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
@@ -135,22 +122,31 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
match focus_kind {
WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed),
WindowKind::Single => FOCUSED.load(Ordering::Relaxed),
WindowKind::Stack => STACK.load(Ordering::Relaxed),
WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed),
WindowKind::Floating => FLOATING.load(Ordering::Relaxed),
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
WindowKind::Floating => FLOATING.load(Ordering::SeqCst),
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
std::thread::spawn(move || {
unsafe {
if let Err(error) = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)
{
tracing::error!("{error}");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
}
});
@@ -159,6 +155,7 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
let receiver = event_rx();
event_tx().send(Notification(None))?;
@@ -175,6 +172,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let focused_workspace_idx =
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
let monitors = state.monitors.clone();
let weak_pending_move_op = Arc::downgrade(&state.pending_move_op);
let pending_move_op = *state.pending_move_op;
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
[focused_workspace_idx]
@@ -182,7 +180,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
drop(state);
@@ -224,16 +221,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?;
}
for window in ws.floating_windows() {
let mut window_kind = WindowKind::Unfocused;
if foreground_window == window.hwnd {
window_kind = WindowKind::Floating;
}
window.set_accent(window_kind_colour(window_kind))?;
}
}
}
}
@@ -262,20 +249,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
should_process_notification = true;
}
// when we switch focus to/from a floating window
let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
// if we switch focus to a floating window
fw == &notification.0.unwrap_or_default() ||
// if there is any floating window with a `WindowKind::Floating` border
// that no longer is the foreground window then we need to update that
// border.
(fw != &foreground_window
&& window_border(*fw)
.map(|b| b.window_kind == WindowKind::Floating)
.unwrap_or_default())
});
if !should_process_notification && switch_focus_to_from_floating_window {
// when we switch focus to a floating window
if !should_process_notification
&& floating_window_hwnds.contains(&notification.0.unwrap_or_default())
{
should_process_notification = true;
}
@@ -294,10 +271,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();
let mut windows_borders = WINDOWS_BORDERS.lock();
// If borders are disabled
if !BORDER_ENABLED.load_consume()
// Or if they are temporarily disabled
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
// Or if the wm is paused
|| is_paused
// Or if we are handling an alt-tab across workspaces
@@ -309,7 +287,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
borders.clear();
windows_borders.clear();
previous_is_paused = is_paused;
continue 'receiver;
@@ -339,15 +316,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let mut new_border = false;
let border = match borders.entry(monocle.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(
monocle.id(),
monocle.focused_window().copied().unwrap_or_default().hwnd,
) {
new_border = true;
if let Ok(border) = Border::create(monocle.id()) {
entry.insert(border)
} else {
continue 'monitors;
@@ -355,33 +327,25 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
let new_focus_state = if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
};
border.window_kind = new_focus_state;
borders_monitors.insert(monocle.id().clone(), monitor_idx);
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(border.hwnd, new_focus_state);
focus_state.insert(
border.hwnd,
if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
},
);
}
let reference_hwnd =
monocle.focused_window().copied().unwrap_or_default().hwnd;
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd,
)?;
let rect = WindowsApi::window_rect(reference_hwnd)?;
if new_border {
border.set_position(&rect, reference_hwnd)?;
}
border.invalidate();
borders_monitors.insert(monocle.id().clone(), monitor_idx);
windows_borders.insert(
monocle.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
border.update(&rect, true)?;
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
@@ -402,11 +366,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
continue 'monitors;
}
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
let foreground_monitor_id =
WindowsApi::monitor_from_window(foreground_hwnd);
let is_maximized = foreground_monitor_id == m.id()
&& WindowsApi::is_zoomed(foreground_hwnd);
let is_maximized = WindowsApi::is_zoomed(
WindowsApi::foreground_window().unwrap_or_default(),
);
if is_maximized {
let mut to_remove = vec![];
@@ -451,17 +413,66 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
borders.remove(id);
}
'containers: for (idx, c) in ws.containers().iter().enumerate() {
for (idx, c) in ws.containers().iter().enumerate() {
let hwnd = c.focused_window().copied().unwrap_or_default().hwnd;
let notification_hwnd = notification.0.unwrap_or_default();
// Update border when moving or resizing with mouse
if pending_move_op.is_some()
&& idx == ws.focused_container_idx()
&& hwnd == notification_hwnd
{
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
let mut rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
// We create a new variable to track the actual pending move op so
// that the other variable `pending_move_op` still holds the
// pending move info so that when the move ends we know on the next
// notification that the previous pending move and pending move are
// different (because a move just finished) and still handle the
// notification. If otherwise we updated the pending_move_op here
// directly then the next pending move after finish would be the
// same because we had already updated it here.
let mut sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
while sync_pending_move_op.is_some() {
sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'monitors;
}
}
};
let new_rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect, false)?;
}
}
Z_ORDER.store(restore_z_order);
continue 'monitors;
}
// Get the border entry for this container from the map or create one
let mut new_border = false;
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(
c.id(),
c.focused_window().copied().unwrap_or_default().hwnd,
) {
new_border = true;
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'monitors;
@@ -469,14 +480,13 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
borders_monitors.insert(c.id().clone(), monitor_idx);
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
|| c.focused_window()
.map(|w| w.hwnd != foreground_window)
.unwrap_or_default()
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
@@ -484,7 +494,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} else {
WindowKind::Single
};
border.window_kind = new_focus_state;
// Update the focused state for all containers on this workspace
{
@@ -492,51 +501,66 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
}
let reference_hwnd =
c.focused_window().copied().unwrap_or_default().hwnd;
// avoid getting into a thread restart loop if we try to look up
// rect info for a window that has been destroyed by the time
// we get here
let rect = match WindowsApi::window_rect(reference_hwnd) {
Ok(rect) => rect,
Err(_) => {
let _ = border.destroy();
borders.remove(c.id());
continue 'containers;
}
};
let rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => last_focus_state != new_focus_state,
};
if new_border || should_invalidate {
border.set_position(&rect, reference_hwnd)?;
}
if should_invalidate {
border.invalidate();
}
borders_monitors.insert(c.id().clone(), monitor_idx);
windows_borders.insert(
c.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
border.update(&rect, should_invalidate)?;
}
{
for window in ws.floating_windows() {
let mut new_border = false;
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
'windows: for window in ws.floating_windows() {
let hwnd = window.hwnd;
let notification_hwnd = notification.0.unwrap_or_default();
if pending_move_op.is_some() && hwnd == notification_hwnd {
let mut rect = WindowsApi::window_rect(hwnd)?;
// Check comment above for containers move
let mut sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
while sync_pending_move_op.is_some() {
sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
let border = match borders.entry(hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) =
Border::create(&hwnd.to_string())
{
entry.insert(border)
} else {
continue 'monitors;
}
}
};
let new_rect = WindowsApi::window_rect(hwnd)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect, true)?;
}
}
Z_ORDER.store(restore_z_order);
continue 'monitors;
}
let border = match borders.entry(window.hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) =
Border::create(&window.hwnd.to_string(), window.hwnd)
if let Ok(border) = Border::create(&window.hwnd.to_string())
{
new_border = true;
entry.insert(border)
} else {
continue 'monitors;
@@ -544,15 +568,32 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
#[allow(unused_assignments)]
let mut last_focus_state = None;
let mut new_focus_state = WindowKind::Unfocused;
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
if foreground_window == window.hwnd {
new_focus_state = WindowKind::Floating;
let mut should_destroy = false;
if let Some(notification_hwnd) = notification.0 {
if notification_hwnd != window.hwnd {
should_destroy = true;
}
}
border.window_kind = new_focus_state;
if WindowsApi::foreground_window().unwrap_or_default()
!= window.hwnd
{
should_destroy = true;
}
if should_destroy {
border.destroy()?;
borders.remove(&window.hwnd.to_string());
borders_monitors.remove(&window.hwnd.to_string());
continue 'windows;
}
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = WindowKind::Floating;
{
let mut focus_state = FOCUS_STATE.lock();
last_focus_state =
@@ -566,17 +607,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Some(last_focus_state) => last_focus_state != new_focus_state,
};
if new_border {
border.set_position(&rect, window.hwnd)?;
}
if should_invalidate {
border.invalidate();
}
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
windows_borders.insert(window.hwnd, border.clone());
border.update(&rect, should_invalidate)?;
}
Z_ORDER.store(restore_z_order);
}
}
}
@@ -592,7 +626,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Ok(())
}
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub enum ZOrder {
Top,
NoTopMost,

View File

@@ -8,7 +8,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Colour {
/// Colour represented as RGB
@@ -51,7 +51,7 @@ impl From<Colour> for Color32 {
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Hex(HexColor);
impl JsonSchema for Hex {
@@ -78,7 +78,7 @@ impl From<Colour> for u32 {
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Rgb {
/// Red
pub r: u32,
@@ -120,8 +120,8 @@ impl From<u32> for Rgb {
fn from(value: u32) -> Self {
Self {
r: value & 0xff,
g: (value >> 8) & 0xff,
b: (value >> 16) & 0xff,
g: value >> 8 & 0xff,
b: value >> 16 & 0xff,
}
}
}

View File

@@ -13,11 +13,11 @@ use windows::core::HRESULT;
use windows::core::HSTRING;
use windows::core::PCWSTR;
use windows::core::PWSTR;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::SIZE;
use windows::Win32::UI::Shell::Common::IObjectArray;
use windows_core::BOOL;
type DesktopID = GUID;
@@ -44,7 +44,7 @@ impl<'a, T: Clone> ComIn<'a, T> {
}
}
impl<T> Deref for ComIn<'_, T> {
impl<'a, T> Deref for ComIn<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data

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

View File

@@ -75,18 +75,6 @@ impl Container {
None
}
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
for (idx, window) in self.windows().iter().enumerate() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(idx);
}
}
}
None
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for window in self.windows() {
if window.hwnd == hwnd {

View File

@@ -6,16 +6,7 @@ use strum::Display;
use strum::EnumString;
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum AnimationStyle {
Linear,

View File

@@ -75,7 +75,7 @@ pub struct IdWithIdentifier {
pub matching_strategy: Option<MatchingStrategy>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, JsonSchema)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum MatchingStrategy {
Legacy,
Equals,

View File

@@ -89,6 +89,7 @@ impl DefaultLayout {
return None;
};
let max_divisor = 1.005;
let mut r = resize.unwrap_or_default();
let resize_delta = delta;
@@ -107,13 +108,15 @@ 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();
if diff < unaltered.right as f32 {
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.left += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.left - -resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.left -= -resize_delta;
}
}
@@ -121,13 +124,15 @@ impl DefaultLayout {
OperationDirection::Up => match sizing {
Sizing::Increase => {
let diff = ((r.top + resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.top += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.top - resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.top -= -resize_delta;
}
}
@@ -135,13 +140,15 @@ impl DefaultLayout {
OperationDirection::Right => match sizing {
Sizing::Increase => {
let diff = ((r.right + resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.right += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.right - resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
let max = unaltered.right as f32 / max_divisor;
if diff < max {
r.right -= resize_delta;
}
}
@@ -149,13 +156,15 @@ impl DefaultLayout {
OperationDirection::Down => match sizing {
Sizing::Increase => {
let diff = ((r.bottom + resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.bottom += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.bottom - resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
let max = unaltered.bottom as f32 / max_divisor;
if diff < max {
r.bottom -= resize_delta;
}
}

View File

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

View File

@@ -14,22 +14,16 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::animation::prefix::AnimationPrefix;
use crate::KomorebiTheme;
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
pub use custom_layout::Column;
pub use custom_layout::ColumnSplit;
pub use custom_layout::ColumnSplitWithCapacity;
pub use custom_layout::ColumnWidth;
pub use custom_layout::CustomLayout;
pub use cycle_direction::CycleDirection;
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;
@@ -42,7 +36,6 @@ 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)]
@@ -83,7 +76,6 @@ pub enum SocketMessage {
Promote,
PromoteFocus,
PromoteWindow(OperationDirection),
EagerFocus(String),
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
@@ -112,7 +104,6 @@ pub enum SocketMessage {
NewWorkspace,
ToggleTiling,
Stop,
StopIgnoreRestore,
TogglePause,
Retile,
RetileWithResizeDimensions,
@@ -123,7 +114,6 @@ pub enum SocketMessage {
CycleFocusMonitor(CycleDirection),
CycleFocusWorkspace(CycleDirection),
FocusMonitorNumber(usize),
FocusMonitorAtCursor,
FocusLastWorkspace,
CloseWorkspace,
FocusWorkspaceNumber(usize),
@@ -149,7 +139,6 @@ pub enum SocketMessage {
NamedWorkspaceLayoutCustomRule(String, usize, PathBuf),
ClearWorkspaceLayoutRules(usize, usize),
ClearNamedWorkspaceLayoutRules(String),
ToggleWorkspaceLayer,
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
@@ -158,10 +147,10 @@ pub enum SocketMessage {
CompleteConfiguration,
AltFocusHack(bool),
Theme(KomorebiTheme),
Animation(bool, Option<AnimationPrefix>),
AnimationDuration(u64, Option<AnimationPrefix>),
Animation(bool),
AnimationDuration(u64),
AnimationFps(u64),
AnimationStyle(AnimationStyle, Option<AnimationPrefix>),
AnimationStyle(AnimationStyle),
#[serde(alias = "ActiveWindowBorder")]
Border(bool),
#[serde(alias = "ActiveWindowBorderColour")]
@@ -186,7 +175,6 @@ pub enum SocketMessage {
StackbarFontFamily(Option<String>),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
ToggleWindowBasedWorkAreaOffset,
ResizeDelta(i32),
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
@@ -195,7 +183,6 @@ pub enum SocketMessage {
ClearWorkspaceRules(usize, usize),
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
EnforceWorkspaceRules,
#[serde(alias = "FloatRule")]
IgnoreRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
@@ -247,9 +234,7 @@ pub struct SubscribeOptions {
pub filter_state_changes: bool,
}
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema, ValueEnum,
)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
Never,
@@ -320,8 +305,6 @@ pub enum BorderImplementation {
ValueEnum,
JsonSchema,
PartialEq,
Eq,
Hash,
)]
pub enum WindowKind {
Single,
@@ -339,7 +322,6 @@ pub enum StateQuery {
FocusedWorkspaceIndex,
FocusedContainerIndex,
FocusedWindowIndex,
FocusedWorkspaceName,
}
#[derive(
@@ -438,16 +420,7 @@ pub enum MoveBehaviour {
}
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum CrossBoundaryBehaviour {
/// Attempt to perform actions across a workspace boundary
@@ -457,16 +430,7 @@ pub enum CrossBoundaryBehaviour {
}
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum HidingBehaviour {
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)

View File

@@ -1,48 +0,0 @@
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

@@ -37,12 +37,6 @@ impl From<Rect> for RECT {
}
}
impl Rect {
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
self.right == rhs.right && self.bottom == rhs.bottom
}
}
impl Rect {
/// decrease the size of self by the padding amount.
pub fn add_padding<T>(&mut self, padding: T)

View File

@@ -1,6 +1,7 @@
#![warn(clippy::all)]
pub mod animation;
pub mod animation_manager;
pub mod border_manager;
pub mod com;
#[macro_use]
@@ -32,7 +33,6 @@ 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;
@@ -47,6 +47,8 @@ use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use animation::*;
pub use animation_manager::*;
pub use colour::*;
pub use core::*;
pub use process_command::*;
@@ -128,7 +130,6 @@ 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()));
@@ -182,7 +183,7 @@ lazy_static! {
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
Arc::new(Mutex::new(HidingBehaviour::Cloak));
Arc::new(Mutex::new(HidingBehaviour::Minimize));
pub static ref HOME_DIR: PathBuf = {
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
let home = PathBuf::from(&home_path);
@@ -215,14 +216,18 @@ lazy_static! {
)
};
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
Arc::new(Mutex::new(AnimationStyle::Linear));
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
// Use app-specific titlebar removal options where possible
// eg. Windows Terminal, IntelliJ IDEA, Firefox
static ref NO_TITLEBAR: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
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);
@@ -233,6 +238,8 @@ pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
@@ -285,7 +292,6 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
Socket(SocketMessage),
Monitor(MonitorNotification),
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -301,9 +307,6 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
| NotificationEvent::Socket(SocketMessage::Theme(_))
| NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(_))
| NotificationEvent::WindowManager(WindowManagerEvent::TitleUpdate(_, _))
| NotificationEvent::WindowManager(WindowManagerEvent::Show(_, _))
| NotificationEvent::WindowManager(WindowManagerEvent::Uncloak(_, _))
);
let notification = &serde_json::to_string(&notification)?;

View File

@@ -7,7 +7,6 @@
clippy::doc_markdown
)]
use std::env::temp_dir;
use std::net::Shutdown;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
@@ -18,9 +17,6 @@ use std::time::Duration;
use clap::Parser;
use color_eyre::Result;
use crossbeam_utils::Backoff;
use komorebi::animation::AnimationEngine;
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
@@ -44,7 +40,6 @@ use komorebi::stackbar_manager;
use komorebi::static_config::StaticConfig;
use komorebi::theme_manager;
use komorebi::transparency_manager;
use komorebi::window_manager::State;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
use komorebi::winevent_listener;
@@ -158,9 +153,6 @@ struct Opts {
/// Path to a static configuration JSON file
#[clap(short, long)]
config: Option<PathBuf>,
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
#[clap(long)]
clean_state: bool,
}
#[tracing::instrument]
@@ -176,8 +168,8 @@ fn main() -> Result<()> {
let session_id = WindowsApi::process_id_to_session_id()?;
SESSION_ID.store(session_id, Ordering::SeqCst);
let mut system = sysinfo::System::new();
system.refresh_processes(ProcessesToUpdate::All, true);
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All);
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
@@ -265,19 +257,6 @@ fn main() -> Result<()> {
}
}
let dumped_state = temp_dir().join("komorebi.state.json");
if !opts.clean_state && dumped_state.is_file() {
if let Ok(state) = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?) {
wm.lock().apply_state(state);
} else {
tracing::warn!(
"cannot apply state from {}; state struct is not up to date",
dumped_state.display()
);
}
}
wm.lock().retile_all(false)?;
listen_for_events(wm.clone());
@@ -291,7 +270,7 @@ fn main() -> Result<()> {
transparency_manager::listen_for_notifications(wm.clone());
workspace_reconciliator::listen_for_notifications(wm.clone());
monitor_reconciliator::listen_for_notifications(wm.clone())?;
reaper::listen_for_notifications(wm.clone());
reaper::watch_for_orphans(wm.clone());
focus_manager::listen_for_notifications(wm.clone());
theme_manager::listen_for_notifications();
@@ -308,13 +287,7 @@ fn main() -> Result<()> {
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
let state = State::from(&*wm.lock());
std::fs::write(dumped_state, serde_json::to_string_pretty(&state)?)?;
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
wm.lock().restore_all_windows(false)?;
AnimationEngine::wait_for_all_animations();
wm.lock().restore_all_windows()?;
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;

View File

@@ -36,58 +36,33 @@ use crate::WindowsApi;
)]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
pub id: isize,
id: isize,
#[getset(get = "pub", set = "pub")]
pub name: String,
name: String,
#[getset(get = "pub", set = "pub")]
pub device: String,
device: String,
#[getset(get = "pub", set = "pub")]
pub device_id: String,
device_id: String,
#[getset(get = "pub", set = "pub")]
pub serial_number_id: Option<String>,
size: Rect,
#[getset(get = "pub", set = "pub")]
pub size: Rect,
#[getset(get = "pub", set = "pub")]
pub work_area_size: Rect,
work_area_size: Rect,
#[getset(get_copy = "pub", set = "pub")]
pub work_area_offset: Option<Rect>,
work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
pub window_based_work_area_offset: Option<Rect>,
window_based_work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
pub window_based_work_area_offset_limit: isize,
pub workspaces: Ring<Workspace>,
window_based_work_area_offset_limit: isize,
workspaces: Ring<Workspace>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
pub last_focused_workspace: Option<usize>,
last_focused_workspace: Option<usize>,
#[getset(get_mut = "pub")]
pub workspace_names: HashMap<usize, String>,
workspace_names: HashMap<usize, String>,
}
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,
@@ -95,7 +70,6 @@ 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());
@@ -105,7 +79,6 @@ pub fn new(
name,
device,
device_id,
serial_number_id,
size,
work_area_size,
work_area_offset: None,
@@ -118,33 +91,12 @@ pub fn new(
}
impl Monitor {
pub fn new(
id: isize,
size: Rect,
work_area_size: Rect,
name: String,
device: String,
device_id: String,
serial_number_id: Option<String>,
) -> Self {
new(
id,
size,
work_area_size,
name,
device,
device_id,
serial_number_id,
)
}
pub fn placeholder() -> Self {
Self {
id: 0,
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,
@@ -155,13 +107,6 @@ 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() {
@@ -194,86 +139,6 @@ impl Monitor {
Ok(())
}
/// Adds a container to this `Monitor` using the move direction to calculate if the container
/// should be added in front of all containers, in the back or in place of the focused
/// container, moving the rest along. The move direction should be from the origin monitor
/// towards the target monitor or from the origin workspace towards the target workspace.
pub fn add_container_with_direction(
&mut self,
container: Container,
workspace_idx: Option<usize>,
direction: OperationDirection,
) -> Result<()> {
let workspace = if let Some(idx) = workspace_idx {
self.workspaces_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
} else {
self.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?
};
match direction {
OperationDirection::Left => {
// insert the container into the workspace on the monitor at the back (or rightmost position)
// if we are moving across a boundary to the left (back = right side of the target)
match workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.insert_container_at_idx(0, container);
} else {
workspace.add_container_to_back(container);
}
}
_ => {
workspace.add_container_to_back(container);
}
},
Layout::Custom(_) => {
workspace.add_container_to_back(container);
}
}
}
OperationDirection::Right => {
// insert the container into the workspace on the monitor at the front (or leftmost position)
// if we are moving across a boundary to the right (front = left side of the target)
match workspace.layout() {
Layout::Default(layout) => {
let target_index = layout.leftmost_index(workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.add_container_to_back(container);
} else {
workspace.insert_container_at_idx(target_index, container);
}
}
_ => {
workspace.insert_container_at_idx(target_index, container);
}
}
}
Layout::Custom(_) => {
workspace.add_container_to_front(container);
}
}
}
OperationDirection::Up | OperationDirection::Down => {
// insert the container into the workspace on the monitor at the position
// where the currently focused container on that workspace is
workspace.insert_container_at_idx(workspace.focused_container_idx(), container);
}
};
Ok(())
}
pub fn remove_workspace_by_idx(&mut self, idx: usize) -> Option<Workspace> {
if idx < self.workspaces().len() {
return self.workspaces_mut().remove(idx);
@@ -350,14 +215,54 @@ impl Monitor {
Some(workspace) => workspace,
};
if let Some(direction) = direction {
self.add_container_with_direction(
container,
Some(target_workspace_idx),
direction,
)?;
} else {
target_workspace.add_container_to_back(container);
match direction {
Some(OperationDirection::Left) => match target_workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
target_workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.insert_container_at_idx(0, container);
} else {
target_workspace.add_container_to_back(container);
}
}
_ => {
target_workspace.add_container_to_back(container);
}
},
Layout::Custom(_) => {
target_workspace.add_container_to_back(container);
}
},
Some(OperationDirection::Right) => match target_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(target_workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.add_container_to_back(container);
} else {
target_workspace
.insert_container_at_idx(target_index, container);
}
}
_ => {
target_workspace.insert_container_at_idx(target_index, container);
}
}
}
Layout::Custom(_) => {
target_workspace.add_container_to_front(container);
}
},
_ => {
target_workspace.add_container_to_back(container);
}
}
}

View File

@@ -2,33 +2,21 @@ use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::System::Power::POWERBROADCAST_SETTING;
use windows::Win32::System::SystemServices::GUID_LIDSWITCH_STATE_CHANGE;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::DBT_CONFIGCHANGED;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEARRIVAL;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEREMOVECOMPLETE;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVTYP_DEVICEINTERFACE;
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
use windows::Win32::UI::WindowsAndMessaging::PBT_POWERSETTINGCHANGE;
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
@@ -87,7 +75,7 @@ impl Hidden {
loop {
unsafe {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
tracing::debug!("hidden window event processing thread shutdown");
break;
};
@@ -104,57 +92,8 @@ impl Hidden {
let hwnd = hwnd_receiver.recv()?;
// Register Session Lock/Unlock events
WindowsApi::wts_register_session_notification(hwnd)?;
// Register Laptop lid open/close events
WindowsApi::register_power_setting_notification(
hwnd,
&GUID_LIDSWITCH_STATE_CHANGE,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
// Register device interface events for multiple display related devices. Some of this
// device interfaces might not be needed but it doesn't hurt to have them in case some user
// uses some output device as monitor that falls into one of these device interface class
// GUID.
let monitor_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
dbcc_reserved: 0,
dbcc_classguid: GUID_DEVINTERFACE_MONITOR,
dbcc_name: [0; 1],
};
let display_adapter_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
dbcc_reserved: 0,
dbcc_classguid: GUID_DEVINTERFACE_DISPLAY_ADAPTER,
dbcc_name: [0; 1],
};
let video_output_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
dbcc_reserved: 0,
dbcc_classguid: GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL,
dbcc_name: [0; 1],
};
WindowsApi::register_device_notification(
hwnd,
monitor_filter,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
WindowsApi::register_device_notification(
hwnd,
display_adapter_filter,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
WindowsApi::register_device_notification(
hwnd,
video_output_filter,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
Ok(Self { hwnd })
}
@@ -175,7 +114,7 @@ impl Hidden {
"WM_POWERBROADCAST event received - resume from suspend"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::ResumingFromSuspendedState,
monitor_reconciliator::Notification::ResumingFromSuspendedState,
);
LRESULT(0)
}
@@ -185,39 +124,10 @@ impl Hidden {
"WM_POWERBROADCAST event received - entering suspended state"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::EnteringSuspendedState,
monitor_reconciliator::Notification::EnteringSuspendedState,
);
LRESULT(0)
}
// Monitor change power status
PBT_POWERSETTINGCHANGE => {
if let POWERBROADCAST_SETTING {
PowerSetting: GUID_LIDSWITCH_STATE_CHANGE,
DataLength: _,
Data: [0],
} = *(lparam.0 as *const POWERBROADCAST_SETTING)
{
tracing::debug!(
"WM_POWERBROADCAST event received - laptop lid closed"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
);
} else if let POWERBROADCAST_SETTING {
PowerSetting: GUID_LIDSWITCH_STATE_CHANGE,
DataLength: _,
Data: [1],
} = *(lparam.0 as *const POWERBROADCAST_SETTING)
{
tracing::debug!(
"WM_POWERBROADCAST event received - laptop lid opened"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
);
}
LRESULT(0)
}
_ => LRESULT(0),
}
}
@@ -227,14 +137,14 @@ impl Hidden {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::SessionLocked,
monitor_reconciliator::Notification::SessionLocked,
);
}
WTS_SESSION_UNLOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::SessionUnlocked,
monitor_reconciliator::Notification::SessionUnlocked,
);
}
_ => {}
@@ -255,7 +165,7 @@ impl Hidden {
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::ResolutionScalingChanged,
monitor_reconciliator::Notification::ResolutionScalingChanged,
);
LRESULT(0)
}
@@ -269,7 +179,7 @@ impl Hidden {
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::WorkAreaChanged,
monitor_reconciliator::Notification::WorkAreaChanged,
);
}
LRESULT(0)
@@ -278,17 +188,12 @@ impl Hidden {
// Original idea from https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
let event = wparam.0 as u32;
if event == DBT_DEVNODES_CHANGED
|| event == DBT_CONFIGCHANGED
|| event == DBT_DEVICEARRIVAL
|| event == DBT_DEVICEREMOVECOMPLETE
{
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
tracing::debug!(
"WM_DEVICECHANGE event received with one of [DBT_DEVNODES_CHANGED, DBT_CONFIGCHANGED, DBT_DEVICEARRIVAL, DBT_DEVICEREMOVECOMPLETE] - display added or removed"
);
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
monitor_reconciliator::Notification::DisplayConnectionChange,
);
}

View File

@@ -1,25 +1,17 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::Rect;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
use crate::notify_subscribers;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::MonitorConfig;
use crate::WindowManager;
use crate::WindowsApi;
use crate::WORKSPACE_MATCHING_RULES;
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;
@@ -28,9 +20,7 @@ use std::sync::OnceLock;
pub mod hidden;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "content")]
pub enum MonitorNotification {
pub enum Notification {
ResolutionScalingChanged,
WorkAreaChanged,
DisplayConnectionChange,
@@ -42,35 +32,34 @@ pub enum MonitorNotification {
static ACTIVE: AtomicBool = AtomicBool::new(true);
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
OnceLock::new();
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, Monitor>>> = OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = OnceLock::new();
pub fn channel() -> &'static (Sender<MonitorNotification>, Receiver<MonitorNotification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
}
fn event_tx() -> Sender<MonitorNotification> {
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<MonitorNotification> {
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification(notification: MonitorNotification) {
pub fn send_notification(notification: Notification) {
if event_tx().try_send(notification).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
monitor_cache.insert(serial_or_device_id.to_string(), monitor);
monitor_cache.insert(device_id.to_string(), config);
}
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
@@ -100,12 +89,10 @@ 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")?;
@@ -129,40 +116,40 @@ 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");
let receiver = event_rx();
'receiver: for notification in receiver {
if !ACTIVE.load_consume()
&& matches!(
if !ACTIVE.load_consume() {
if matches!(
notification,
MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked
)
{
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
);
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked
) {
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
);
ACTIVE.store(true, Ordering::SeqCst);
border_manager::send_notification(None);
ACTIVE.store(true, Ordering::SeqCst);
}
continue 'receiver;
}
let mut wm = wm.lock();
let initial_state = State::from(wm.as_ref());
match notification {
MonitorNotification::EnteringSuspendedState | MonitorNotification::SessionLocked => {
Notification::EnteringSuspendedState | Notification::SessionLocked => {
tracing::debug!(
"deactivating reconciliator until system resumes from suspended state or session is unlocked"
);
ACTIVE.store(false, Ordering::SeqCst);
}
MonitorNotification::WorkAreaChanged => {
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => {
// this is only handled above if the reconciliator is paused
}
Notification::WorkAreaChanged => {
tracing::debug!("handling work area changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
@@ -194,7 +181,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
}
}
MonitorNotification::ResolutionScalingChanged => {
Notification::ResolutionScalingChanged => {
tracing::debug!("handling resolution/scaling changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
@@ -241,12 +228,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
}
}
// this is handled above if the reconciliator is paused but we should still check if
// there were any changes to the connected monitors while the system was
// suspended/locked.
MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked
| MonitorNotification::DisplayConnectionChange => {
Notification::DisplayConnectionChange => {
tracing::debug!("handling display connection change notification");
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
@@ -260,12 +242,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Make sure that in our state any attached displays have the latest Win32 data
for monitor in wm.monitors_mut() {
for attached in &attached_devices {
if attached.serial_number_id().eq(monitor.serial_number_id())
|| attached.device_id().eq(monitor.device_id())
{
if attached.device_id().eq(monitor.device_id()) {
monitor.set_id(attached.id());
monitor.set_device_id(attached.device_id().clone());
monitor.set_serial_number_id(attached.serial_number_id().clone());
monitor.set_name(attached.name().clone());
monitor.set_size(*attached.size());
monitor.set_work_area_size(*attached.work_area_size());
@@ -275,7 +253,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if initial_monitor_count == attached_devices.len() {
tracing::debug!("monitor counts match, reconciliation not required");
drop(wm);
continue 'receiver;
}
@@ -283,7 +260,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
tracing::debug!(
"no devices found, skipping reconciliation to avoid breaking state"
);
drop(wm);
continue 'receiver;
}
@@ -293,108 +269,41 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
attached_devices.len()
);
// Windows to remove from `known_hwnds`
let mut windows_to_remove = Vec::new();
// Gather all the containers that will be orphaned from disconnected and invalid displays
let mut orphaned_containers = vec![];
// Collect the ids in our state which aren't in the current attached display ids
// These are monitors that have been removed
let mut newly_removed_displays = vec![];
for (m_idx, m) in wm.monitors().iter().enumerate() {
if !attached_devices.iter().any(|attached| {
attached.serial_number_id().eq(m.serial_number_id())
|| attached.device_id().eq(m.device_id())
}) {
let id = m
.serial_number_id()
.as_ref()
.map_or(m.device_id().clone(), |sn| sn.clone());
newly_removed_displays.push(id.clone());
let focused_workspace_idx = m.focused_workspace_idx();
for (idx, workspace) in m.workspaces().iter().enumerate() {
let is_focused_workspace = idx == focused_workspace_idx;
let focused_container_idx = workspace.focused_container_idx();
for (c_idx, container) in workspace.containers().iter().enumerate()
{
let focused_window_idx = container.focused_window_idx();
for (w_idx, window) in container.windows().iter().enumerate() {
windows_to_remove.push(window.hwnd);
if is_focused_workspace
&& c_idx == focused_container_idx
&& w_idx == focused_window_idx
{
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if window.is_focused() {
window.minimize();
}
}
}
for m in wm.monitors().iter() {
if !attached_devices
.iter()
.any(|attached| attached.device_id().eq(m.device_id()))
{
newly_removed_displays.push(m.device_id().clone());
for workspace in m.workspaces() {
for container in workspace.containers() {
// Save the orphaned containers from the removed monitor
orphaned_containers.push(container.clone());
}
if let Some(maximized) = workspace.maximized_window() {
windows_to_remove.push(maximized.hwnd);
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if maximized.is_focused() {
maximized.minimize();
}
}
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
windows_to_remove.push(window.hwnd);
}
if let Some(window) = container.focused_window() {
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if window.is_focused() {
window.minimize();
}
}
}
for window in workspace.floating_windows() {
windows_to_remove.push(window.hwnd);
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if window.is_focused() {
window.minimize();
}
}
}
// Remove any workspace_rules for this specific monitor
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
let mut rules_to_remove = Vec::new();
for (i, rule) in workspace_rules.iter().enumerate().rev() {
if rule.monitor_index == m_idx {
rules_to_remove.push(i);
}
}
for i in rules_to_remove {
workspace_rules.remove(i);
}
// Let's add their state to the cache for later
monitor_cache.insert(id, m.clone());
monitor_cache.insert(m.device_id().clone(), m.into());
}
}
// Update known_hwnds
wm.known_hwnds.retain(|i, _| !windows_to_remove.contains(i));
if !orphaned_containers.is_empty() {
tracing::info!(
"removed orphaned containers from: {newly_removed_displays:?}"
);
}
if !newly_removed_displays.is_empty() {
// After we have cached them, remove them from our state
wm.monitors_mut().retain(|m| {
!newly_removed_displays.iter().any(|id| {
m.serial_number_id().as_ref().is_some_and(|sn| sn == id)
|| m.device_id() == id
})
});
wm.monitors_mut()
.retain(|m| !newly_removed_displays.contains(m.device_id()));
}
let post_removal_monitor_count = wm.monitors().len();
@@ -403,6 +312,24 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
wm.focus_monitor(0)?;
}
if !orphaned_containers.is_empty() {
if let Some(primary) = wm.monitors_mut().front_mut() {
if let Some(focused_ws) = primary.focused_workspace_mut() {
let focused_container_idx = focused_ws.focused_container_idx();
// Put the orphaned containers somewhere visible
for container in orphaned_containers {
focused_ws.add_container_to_back(container);
}
// Gotta reset the focus or the movement will feel "off"
if initial_monitor_count != post_removal_monitor_count {
focused_ws.focus_container(focused_container_idx);
}
}
}
}
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
@@ -415,11 +342,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let post_removal_monitor_count = wm.monitors().len();
// This is the list of device ids after we have removed detached displays. We can
// keep this with just the device_ids without the serial numbers since this is used
// only to check which one is the newly added monitor below if there is a new
// monitor. Everything done after with said new monitor will again consider both
// serial number and device ids.
// This is the list of device ids after we have removed detached displays
let post_removal_device_ids = wm
.monitors()
.iter()
@@ -429,7 +352,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Check for and add any new monitors that may have been plugged in
// Monitor and display index preferences get applied in this function
WindowsApi::load_monitor_information(&mut wm)?;
WindowsApi::load_monitor_information(&mut wm.monitors)?;
let post_addition_monitor_count = wm.monitors().len();
@@ -438,181 +361,42 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
"monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors",
);
let known_hwnds = wm.known_hwnds.clone();
let offset = wm.work_area_offset;
let mouse_follows_focus = wm.mouse_follows_focus;
let focused_monitor_idx = wm.focused_monitor_idx();
let focused_workspace_idx = wm.focused_workspace_idx()?;
// Look in the updated state for new monitors
for (i, m) in wm.monitors_mut().iter_mut().enumerate() {
let device_id = m.device_id();
for m in wm.monitors_mut() {
let device_id = m.device_id().clone();
// We identify a new monitor when we encounter a new device id
if !post_removal_device_ids.contains(device_id) {
if !post_removal_device_ids.contains(&device_id) {
let mut cache_hit = false;
let mut cached_id = String::new();
// Check if that device id exists in the cache for this session
if let Some((id, cached)) = monitor_cache.get_key_value(device_id).or(m
.serial_number_id()
.as_ref()
.and_then(|sn| monitor_cache.get_key_value(sn)))
{
if let Some(cached) = monitor_cache.get(&device_id) {
cache_hit = true;
cached_id = id.clone();
tracing::info!("found monitor and workspace configuration for {id} in the monitor cache, applying");
tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying");
// If it does, load the monitor removing any window that has since
// been closed or moved to another workspace
*m = cached.clone();
// If it does, load all the monitor settings from the cache entry
m.ensure_workspace_count(cached.workspaces.len());
m.set_work_area_offset(cached.work_area_offset);
m.set_window_based_work_area_offset(
cached.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
cached.window_based_work_area_offset_limit.unwrap_or(1),
);
let focused_workspace_idx = m.focused_workspace_idx();
for (j, workspace) in m.workspaces_mut().iter_mut().enumerate() {
// If this is the focused workspace we need to show (restore) all
// windows that were visible since they were probably minimized by
// Windows.
let is_focused_workspace = j == focused_workspace_idx;
let focused_container_idx = workspace.focused_container_idx();
let mut empty_containers = Vec::new();
for (idx, container) in
workspace.containers_mut().iter_mut().enumerate()
{
container.windows_mut().retain(|window| {
window.exe().is_ok()
&& !known_hwnds.contains_key(&window.hwnd)
});
if container.windows().is_empty() {
empty_containers.push(idx);
}
if is_focused_workspace {
if let Some(window) = container.focused_window() {
tracing::debug!(
"restoring window: {}",
window.hwnd
);
WindowsApi::restore_window(window.hwnd);
} else {
// If the focused window was moved or removed by
// the user after the disconnect then focus the
// first window and show that one
container.focus_window(0);
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
}
}
}
}
// Remove empty containers
for empty_idx in empty_containers {
if empty_idx == focused_container_idx {
workspace.remove_container(empty_idx);
} else {
workspace.remove_container_by_idx(empty_idx);
}
}
if let Some(window) = workspace.maximized_window() {
if window.exe().is_err()
|| known_hwnds.contains_key(&window.hwnd)
{
workspace.set_maximized_window(None);
} else if is_focused_workspace {
WindowsApi::restore_window(window.hwnd);
}
}
if let Some(container) = workspace.monocle_container_mut() {
container.windows_mut().retain(|window| {
window.exe().is_ok()
&& !known_hwnds.contains_key(&window.hwnd)
});
if container.windows().is_empty() {
workspace.set_monocle_container(None);
} else if is_focused_workspace {
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
} else {
// If the focused window was moved or removed by
// the user after the disconnect then focus the
// first window and show that one
container.focus_window(0);
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
}
}
}
}
workspace.floating_windows_mut().retain(|window| {
window.exe().is_ok()
&& !known_hwnds.contains_key(&window.hwnd)
});
if is_focused_workspace {
for window in workspace.floating_windows() {
WindowsApi::restore_window(window.hwnd);
}
}
// Apply workspace rules
let mut workspace_matching_rules =
WORKSPACE_MATCHING_RULES.lock();
if let Some(rules) = workspace
.workspace_config()
.as_ref()
.and_then(|c| c.workspace_rules.as_ref())
{
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: false,
});
}
}
if let Some(rules) = workspace
.workspace_config()
.as_ref()
.and_then(|c| c.initial_workspace_rules.as_ref())
{
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: true,
});
}
for (w_idx, workspace) in m.workspaces_mut().iter_mut().enumerate()
{
if let Some(cached_workspace) = cached.workspaces.get(w_idx) {
workspace.load_static_config(cached_workspace)?;
}
}
// Restore windows from new monitor and update the focused
// workspace
m.load_focused_workspace(mouse_follows_focus)?;
m.update_focused_workspace(offset)?;
}
// Entries in the cache should only be used once; remove the entry there was a cache hit
if cache_hit && !cached_id.is_empty() {
monitor_cache.remove(&cached_id);
if cache_hit {
monitor_cache.remove(&device_id);
}
}
}
// Refocus the previously focused monitor since the code above might
// steal the focus away.
wm.focus_monitor(focused_monitor_idx)?;
wm.focus_workspace(focused_workspace_idx)?;
}
let final_count = wm.monitors().len();
@@ -626,14 +410,6 @@ 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

@@ -4,6 +4,7 @@ use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::net::Shutdown;
use std::net::TcpListener;
use std::net::TcpStream;
use std::num::NonZeroUsize;
@@ -21,9 +22,6 @@ use schemars::gen::SchemaSettings;
use schemars::schema_for;
use uds_windows::UnixStream;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
@@ -42,17 +40,12 @@ use crate::core::StateQuery;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowKind;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::colour::Rgb;
use crate::config_generation::WorkspaceMatchingRule;
use crate::current_virtual_desktop;
use crate::monitor::MonitorInformation;
use crate::notify_subscribers;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
@@ -66,12 +59,14 @@ use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::WorkspaceLayer;
use crate::workspace::WorkspaceWindowLocation;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
@@ -232,97 +227,14 @@ impl WindowManager {
self.focus_container_in_direction(direction)?;
self.promote_container_to_front()?
}
SocketMessage::EagerFocus(ref exe) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx()?;
let mut window_location = None;
let mut monitor_workspace_indices = None;
'search: for (monitor_idx, monitor) in self.monitors().iter().enumerate() {
for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() {
if let Some(location) = workspace.location_from_exe(exe) {
window_location = Some(location);
monitor_workspace_indices = Some((monitor_idx, workspace_idx));
break 'search;
}
}
}
if let Some((monitor_idx, workspace_idx)) = monitor_workspace_indices {
if monitor_idx != focused_monitor_idx {
self.focus_monitor(monitor_idx)?;
}
if workspace_idx != focused_workspace_idx {
self.focus_workspace(workspace_idx)?;
}
}
if let Some(location) = window_location {
match location {
WorkspaceWindowLocation::Monocle(window_idx) => {
self.focus_container_window(window_idx)?;
}
WorkspaceWindowLocation::Maximized => {
if let Some(window) =
self.focused_workspace_mut()?.maximized_window_mut()
{
window.focus(self.mouse_follows_focus)?;
}
}
WorkspaceWindowLocation::Container(container_idx, window_idx) => {
let focused_container_idx = self.focused_container_idx()?;
if container_idx != focused_container_idx {
self.focused_workspace_mut()?.focus_container(container_idx);
}
self.focus_container_window(window_idx)?;
}
WorkspaceWindowLocation::Floating(window_idx) => {
if let Some(window) = self
.focused_workspace_mut()?
.floating_windows_mut()
.get_mut(window_idx)
{
window.focus(self.mouse_follows_focus)?;
}
}
}
}
}
SocketMessage::FocusWindow(direction) => {
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer() {
WorkspaceLayer::Tiling => {
self.focus_container_in_direction(direction)?;
}
WorkspaceLayer::Floating => {
self.focus_floating_window_in_direction(direction)?;
}
}
self.focus_container_in_direction(direction)?;
}
SocketMessage::MoveWindow(direction) => {
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer() {
WorkspaceLayer::Tiling => {
self.move_container_in_direction(direction)?;
}
WorkspaceLayer::Floating => {
self.move_floating_window_in_direction(direction)?;
}
}
self.move_container_in_direction(direction)?;
}
SocketMessage::CycleFocusWindow(direction) => {
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer() {
WorkspaceLayer::Tiling => {
self.focus_container_in_cycle_direction(direction)?;
}
WorkspaceLayer::Floating => {
self.focus_floating_window_in_cycle_direction(direction)?;
}
}
self.focus_container_in_cycle_direction(direction)?;
}
SocketMessage::CycleMoveWindow(direction) => {
self.move_container_in_cycle_direction(direction)?;
@@ -482,13 +394,6 @@ impl WindowManager {
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_rules.clear();
}
SocketMessage::EnforceWorkspaceRules => {
{
let mut already_moved = self.already_moved_window_handles.lock();
already_moved.clear();
}
self.enforce_workspace_rules()?;
}
SocketMessage::ManageRule(identifier, ref id) => {
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
@@ -627,8 +532,7 @@ impl WindowManager {
self.move_container_to_workspace(workspace_idx, true, None)?;
}
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
self.move_container_to_monitor(monitor_idx, None, true)?;
}
SocketMessage::SwapWorkspacesToMonitorNumber(monitor_idx) => {
self.swap_focused_monitor(monitor_idx)?;
@@ -640,8 +544,7 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
self.move_container_to_monitor(monitor_idx, None, true)?;
}
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, false, None)?;
@@ -663,8 +566,7 @@ impl WindowManager {
self.move_container_to_workspace(workspace_idx, false, None)?;
}
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
self.move_container_to_monitor(monitor_idx, None, false)?;
}
SocketMessage::CycleSendContainerToMonitor(direction) => {
let monitor_idx = direction.next_idx(
@@ -673,37 +575,22 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
self.move_container_to_monitor(monitor_idx, None, false)?;
}
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
false,
direction,
)?;
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
}
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
true,
direction,
)?;
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
}
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
false,
direction,
)?;
}
}
@@ -711,13 +598,7 @@ impl WindowManager {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
true,
direction,
)?;
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
}
}
@@ -760,16 +641,13 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::FocusMonitorAtCursor => {
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
}
}
SocketMessage::Retile => {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
border_manager::destroy_all_borders()?;
self.retile_all(false)?
}
SocketMessage::RetileWithResizeDimensions => {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
border_manager::destroy_all_borders()?;
self.retile_all(true)?
}
@@ -878,15 +756,7 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
self.focus_monitor(monitor_idx)?;
}
let focused_monitor = self
@@ -909,22 +779,16 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
self.focus_monitor(monitor_idx)?;
}
let mut can_close = false;
if let Some(monitor) = self.focused_monitor_mut() {
let focused_workspace_idx = monitor.focused_workspace_idx();
let next_focused_workspace_idx = focused_workspace_idx.saturating_sub(1);
let last_focused_workspace = monitor
.last_focused_workspace()
.unwrap_or(focused_workspace_idx.saturating_sub(1));
if let Some(workspace) = monitor.focused_workspace() {
if monitor.workspaces().len() > 1
@@ -944,7 +808,7 @@ impl WindowManager {
.remove(focused_workspace_idx)
.is_some()
{
self.focus_workspace(next_focused_workspace_idx)?;
self.focus_workspace(last_focused_workspace)?;
}
}
}
@@ -953,15 +817,7 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
self.focus_monitor(monitor_idx)?;
}
let idx = self
@@ -984,15 +840,7 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
self.focus_monitor(monitor_idx)?;
}
if self.focused_workspace_idx().unwrap_or_default() != workspace_idx {
@@ -1004,15 +852,7 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
self.focus_monitor(monitor_idx)?;
}
let focused_monitor_idx = self.focused_monitor_idx();
@@ -1045,34 +885,27 @@ impl WindowManager {
self.focus_workspace(workspace_idx)?;
}
}
SocketMessage::ToggleWorkspaceLayer => {
let mouse_follows_focus = self.mouse_follows_focus;
let workspace = self.focused_workspace_mut()?;
match workspace.layer() {
WorkspaceLayer::Tiling => {
workspace.set_layer(WorkspaceLayer::Floating);
if let Some(first) = workspace.floating_windows().first() {
first.focus(mouse_follows_focus)?;
}
}
WorkspaceLayer::Floating => {
workspace.set_layer(WorkspaceLayer::Tiling);
if let Some(container) = workspace.focused_container() {
if let Some(window) = container.focused_window() {
window.focus(mouse_follows_focus)?;
}
}
}
};
}
SocketMessage::Stop => {
self.stop(false)?;
}
SocketMessage::StopIgnoreRestore => {
self.stop(true)?;
tracing::info!(
"received stop command, restoring all hidden windows and terminating process"
);
self.restore_all_windows()?;
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
}
let sockets = SUBSCRIPTION_SOCKETS.lock();
for path in (*sockets).values() {
if let Ok(stream) = UnixStream::connect(path) {
stream.shutdown(Shutdown::Both)?;
}
}
let socket = DATA_DIR.join("komorebi.sock");
let _ = std::fs::remove_file(socket);
std::process::exit(0)
}
SocketMessage::MonitorIndexPreference(index_preference, left, top, right, bottom) => {
let mut monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
@@ -1145,9 +978,9 @@ impl WindowManager {
reply.write_all(visible_windows_state.as_bytes())?;
}
SocketMessage::MonitorInformation => {
let mut monitors = vec![];
let mut monitors = HashMap::new();
for monitor in self.monitors() {
monitors.push(MonitorInformation::from(monitor));
monitors.insert(monitor.device_id(), monitor.size());
}
let monitors_state = serde_json::to_string_pretty(&monitors)
@@ -1157,29 +990,19 @@ impl WindowManager {
}
SocketMessage::Query(query) => {
let response = match query {
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx().to_string(),
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
StateQuery::FocusedWorkspaceIndex => self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx()
.to_string(),
StateQuery::FocusedContainerIndex => self
.focused_workspace()?
.focused_container_idx()
.to_string(),
.focused_workspace_idx(),
StateQuery::FocusedContainerIndex => {
self.focused_workspace()?.focused_container_idx()
}
StateQuery::FocusedWindowIndex => {
self.focused_container()?.focused_window_idx().to_string()
self.focused_container()?.focused_window_idx()
}
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())
}
};
}
.to_string();
reply.write_all(response.as_bytes())?;
}
@@ -1397,7 +1220,7 @@ impl WindowManager {
// Pause so that restored windows come to the foreground from all workspaces
self.is_paused = true;
// Bring all windows to the foreground
self.restore_all_windows(false)?;
self.restore_all_windows()?;
// Create a new wm from the config path
let mut wm = StaticConfig::preload(
@@ -1409,8 +1232,6 @@ 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)?;
@@ -1506,14 +1327,6 @@ impl WindowManager {
self.retile_all(false)?;
}
}
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
let workspace = self.focused_workspace_mut()?;
workspace.set_apply_window_based_work_area_offset(
!workspace.apply_window_based_work_area_offset(),
);
self.retile_all(false)?;
}
SocketMessage::QuickSave => {
let workspace = self.focused_workspace()?;
let resize = workspace.resize_dimensions();
@@ -1673,16 +1486,6 @@ impl WindowManager {
}
SocketMessage::Border(enable) => {
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
if !enable {
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
border_manager::destroy_all_borders()?;
}
BorderImplementation::Windows => {
self.remove_all_accents()?;
}
}
}
}
SocketMessage::BorderImplementation(implementation) => {
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
@@ -1729,41 +1532,18 @@ impl WindowManager {
SocketMessage::BorderOffset(offset) => {
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
}
SocketMessage::Animation(enable, prefix) => match prefix {
Some(prefix) => {
ANIMATION_ENABLED_PER_ANIMATION
.lock()
.insert(prefix, enable);
}
None => {
ANIMATION_ENABLED_GLOBAL.store(enable, Ordering::SeqCst);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
}
},
SocketMessage::AnimationDuration(duration, prefix) => match prefix {
Some(prefix) => {
ANIMATION_DURATION_PER_ANIMATION
.lock()
.insert(prefix, duration);
}
None => {
ANIMATION_DURATION_GLOBAL.store(duration, Ordering::SeqCst);
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
}
},
SocketMessage::Animation(enable) => {
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::AnimationDuration(duration) => {
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
}
SocketMessage::AnimationFps(fps) => {
ANIMATION_FPS.store(fps, Ordering::SeqCst);
}
SocketMessage::AnimationStyle(style, prefix) => match prefix {
Some(prefix) => {
ANIMATION_STYLE_PER_ANIMATION.lock().insert(prefix, style);
}
None => {
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
*animation_style = style;
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
}
},
SocketMessage::AnimationStyle(style) => {
*ANIMATION_STYLE.lock() = style;
}
SocketMessage::ToggleTransparency => {
let current = transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED.store(!current, Ordering::SeqCst);
@@ -1776,7 +1556,6 @@ impl WindowManager {
}
SocketMessage::StackbarMode(mode) => {
STACKBAR_MODE.store(mode);
self.retile_all(true)?;
}
SocketMessage::StackbarLabel(label) => {
STACKBAR_LABEL.store(label);
@@ -1842,24 +1621,10 @@ impl WindowManager {
reply.write_all(config.as_bytes())?;
}
SocketMessage::RemoveTitleBar(identifier, ref id) => {
SocketMessage::RemoveTitleBar(_, ref id) => {
let mut identifiers = NO_TITLEBAR.lock();
let mut should_push = true;
for i in &*identifiers {
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
}));
if !identifiers.contains(id) {
identifiers.push(id.clone());
}
}
SocketMessage::ToggleTitleBars => {
@@ -1883,9 +1648,6 @@ impl WindowManager {
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
};
// Update list of known_hwnds and their monitor/workspace index pair
self.update_known_hwnds();
notify_subscribers(
Notification {
event: NotificationEvent::Socket(message.clone()),

View File

@@ -1,3 +1,4 @@
use std::fs::OpenOptions;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
@@ -32,6 +33,7 @@ use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::DATA_DIR;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
@@ -91,18 +93,8 @@ impl WindowManager {
.map(|w| w.hwnd)
.collect::<Vec<_>>();
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(_, _))
if w.contains_managed_window(event_hwnd)
&& !visible_hwnds.contains(&event_hwnd)
{
transparency_override = true;
}
@@ -252,12 +244,7 @@ impl WindowManager {
already_moved_window_handles.remove(&window.hwnd);
}
WindowManagerEvent::FocusChange(_, window) => {
// don't want to trigger the full workspace updates when there are no managed
// containers - this makes floating windows on empty workspaces go into very
// annoying focus change loops which prevents users from interacting with them
if !self.focused_workspace()?.containers().is_empty() {
self.update_focused_workspace(self.mouse_follows_focus, false)?;
}
self.update_focused_workspace(self.mouse_follows_focus, false)?;
let workspace = self.focused_workspace_mut()?;
let floating_window_idx = workspace
@@ -305,28 +292,30 @@ impl WindowManager {
let mut needs_reconciliation = false;
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
if focused_pair != (*m_idx, *w_idx) {
// At this point we know we are going to send a notification to the workspace reconciliator
// So we get the topmost window returned by EnumWindows, which is almost always the window
// that has been selected by alt-tab
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
if let Some(first) =
alt_tab_windows.iter().find(|w| w.title().is_ok())
{
// If our record of this HWND hasn't been updated in over a minute
let mut instant = ALT_TAB_HWND_INSTANT.lock();
if instant.elapsed().gt(&Duration::from_secs(1)) {
// Update our record with the HWND we just found
ALT_TAB_HWND.store(Some(first.hwnd));
// Update the timestamp of our record
*instant = Instant::now();
for (i, monitors) in self.monitors().iter().enumerate() {
for (j, workspace) in monitors.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
// At this point we know we are going to send a notification to the workspace reconciliator
// So we get the topmost window returned by EnumWindows, which is almost always the window
// that has been selected by alt-tab
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
if let Some(first) =
alt_tab_windows.iter().find(|w| w.title().is_ok())
{
// If our record of this HWND hasn't been updated in over a minute
let mut instant = ALT_TAB_HWND_INSTANT.lock();
if instant.elapsed().gt(&Duration::from_secs(1)) {
// Update our record with the HWND we just found
ALT_TAB_HWND.store(Some(first.hwnd));
// Update the timestamp of our record
*instant = Instant::now();
}
}
}
}
workspace_reconciliator::send_notification(*m_idx, *w_idx);
needs_reconciliation = true;
workspace_reconciliator::send_notification(i, j);
needs_reconciliation = true;
}
}
}
@@ -337,14 +326,11 @@ impl WindowManager {
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
if let Some(focused_workspace_idx) = self
.monitors()
.get(*m_idx)
.map(|m| m.focused_workspace_idx())
{
if *m_idx != self.focused_monitor_idx()
&& *w_idx != focused_workspace_idx
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd)
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
@@ -367,11 +353,10 @@ impl WindowManager {
if !workspace_contains_window && !needs_reconciliation {
let floating_applications = FLOATING_APPLICATIONS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut should_float = false;
if !floating_applications.is_empty() {
let regex_identifiers = REGEX_IDENTIFIERS.lock();
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
(window.title(), window.exe(), window.class(), window.path())
{
@@ -500,15 +485,8 @@ impl WindowManager {
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
// This will be true if we have moved to another monitor
let mut moved_across_monitors = false;
if let Some((m_idx, _)) = self.known_hwnds.get(&window.hwnd) {
if *m_idx != target_monitor_idx {
moved_across_monitors = true;
}
}
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
@@ -539,9 +517,7 @@ impl WindowManager {
}
let workspace = self.focused_workspace_mut()?;
if (*workspace.tile() && workspace.contains_managed_window(window.hwnd))
|| moved_across_monitors
{
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
let resize = Rect {
left: new_position.left - old_position.left,
top: new_position.top - old_position.top,
@@ -594,19 +570,11 @@ impl WindowManager {
// so that we don't have ghost tiles until we force an interaction on
// the origin monitor's focused workspace
self.focus_monitor(origin_monitor_idx)?;
let origin_monitor = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
origin_monitor.focus_workspace(origin_workspace_idx)?;
self.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false, false)?;
self.focus_monitor(target_monitor_idx)?;
let target_monitor = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
target_monitor.focus_workspace(target_workspace_idx)?;
self.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false, false)?;
// Make sure to give focus to the moved window again
@@ -709,8 +677,26 @@ impl WindowManager {
window.center(&self.focused_monitor_work_area()?)?;
}
// Update list of known_hwnds and their monitor/workspace index pair
self.update_known_hwnds();
tracing::trace!("updating list of known hwnds");
let mut known_hwnds = vec![];
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
for container in workspace.containers() {
for window in container.windows() {
known_hwnds.push(window.hwnd);
}
}
}
}
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &known_hwnds)?;
notify_subscribers(
Notification {

View File

@@ -1,164 +1,14 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::notify_subscribers;
use crate::winevent::WinEvent;
use crate::NotificationEvent;
use crate::Window;
use crate::WindowManager;
use crate::WindowManagerEvent;
use crate::DATA_DIR;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
lazy_static! {
pub static ref HWNDS_CACHE: Arc<Mutex<HashMap<isize, (usize, usize)>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub struct ReaperNotification(pub HashMap<isize, (usize, usize)>);
static CHANNEL: OnceLock<(Sender<ReaperNotification>, Receiver<ReaperNotification>)> =
OnceLock::new();
pub fn channel() -> &'static (Sender<ReaperNotification>, Receiver<ReaperNotification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
}
fn event_tx() -> Sender<ReaperNotification> {
channel().0.clone()
}
fn event_rx() -> Receiver<ReaperNotification> {
channel().1.clone()
}
pub fn send_notification(hwnds: HashMap<isize, (usize, usize)>) {
if event_tx().try_send(ReaperNotification(hwnds)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
watch_for_orphans(wm.clone());
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
for notification in receiver {
let orphan_hwnds = notification.0;
let mut wm = wm.lock();
let offset = wm.work_area_offset;
let mut update_borders = false;
for (hwnd, (m_idx, w_idx)) in orphan_hwnds.iter() {
if let Some(monitor) = wm.monitors_mut().get_mut(*m_idx) {
let focused_workspace_idx = monitor.focused_workspace_idx();
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
if let Some(workspace) = monitor.workspaces_mut().get_mut(*w_idx) {
// Remove orphan window
if let Err(error) = workspace.remove_window(*hwnd) {
tracing::warn!(
"error reaping orphan window ({}) on monitor: {}, workspace: {}. Error: {}",
hwnd,
m_idx,
w_idx,
error,
);
}
if focused_workspace_idx == *w_idx {
// If this is not a focused workspace there is no need to update the
// workspace or the borders. That will already be done when the user
// changes to this workspace.
workspace.update(&work_area, offset, window_based_work_area_offset)?;
update_borders = true;
}
tracing::info!(
"reaped orphan window ({}) on monitor: {}, workspace: {}",
hwnd,
m_idx,
w_idx,
);
}
}
wm.known_hwnds.remove(hwnd);
let window = Window::from(*hwnd);
notify_subscribers(
crate::Notification {
event: NotificationEvent::WindowManager(WindowManagerEvent::Destroy(
WinEvent::ObjectDestroy,
window,
)),
state: wm.as_ref().into(),
},
true,
)?;
}
if update_borders {
border_manager::send_notification(None);
}
// Save to file
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &wm.known_hwnds.keys().collect::<Vec<_>>())?;
}
Ok(())
}
fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
// Cache current hwnds
{
let mut cache = HWNDS_CACHE.lock();
*cache = wm.lock().known_hwnds.clone();
}
std::thread::spawn(move || loop {
match find_orphans() {
match find_orphans(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
@@ -173,37 +23,44 @@ fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
});
}
fn find_orphans() -> color_eyre::Result<()> {
pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("watching");
let arc = wm.clone();
loop {
std::thread::sleep(Duration::from_millis(20));
std::thread::sleep(Duration::from_secs(1));
let mut cache = HWNDS_CACHE.lock();
let mut orphan_hwnds = HashMap::new();
let mut wm = arc.lock();
let offset = wm.work_area_offset;
for (hwnd, (m_idx, w_idx)) in cache.iter() {
let window = Window::from(*hwnd);
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
if !window.is_window()
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
// docs is closed
//
// I hate every single person who worked on Microsoft Office 365, especially Word
|| !window.is_visible()
{
orphan_hwnds.insert(window.hwnd, (*m_idx, *w_idx));
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
border_manager::send_notification(None);
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
if !orphan_hwnds.is_empty() {
// Update reaper cache
cache.retain(|h, _| !orphan_hwnds.contains_key(h));
// Send handles to remove
event_tx().send(ReaperNotification(orphan_hwnds))?;
}
}
}

View File

@@ -123,7 +123,7 @@ impl Stackbar {
0,
None,
None,
Option::from(HINSTANCE(windows_api::as_ptr!(instance))),
HINSTANCE(windows_api::as_ptr!(instance)),
None,
)?;
@@ -133,7 +133,7 @@ impl Stackbar {
let mut msg: MSG = MSG::default();
loop {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
tracing::debug!("stackbar window event processing thread shutdown");
break;
};
@@ -183,13 +183,13 @@ impl Stackbar {
WindowsApi::position_window(self.hwnd, &layout, false)?;
unsafe {
let hdc = GetDC(Option::from(self.hwnd()));
let hdc = GetDC(self.hwnd());
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
let hbrush = CreateSolidBrush(COLORREF(background));
SelectObject(hdc, hpen.into());
SelectObject(hdc, hbrush.into());
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
SetBkColor(hdc, COLORREF(background));
let mut logfont = LOGFONTW {
@@ -209,14 +209,14 @@ impl Stackbar {
let logical_height = -MulDiv(
STACKBAR_FONT_SIZE.load(Ordering::SeqCst),
72,
GetDeviceCaps(Option::from(hdc), LOGPIXELSY),
GetDeviceCaps(hdc, LOGPIXELSY),
);
logfont.lfHeight = logical_height;
let hfont = CreateFontIndirectW(&logfont);
SelectObject(hdc, hfont.into());
SelectObject(hdc, hfont);
for (i, window) in container.windows().iter().enumerate() {
if window.hwnd == container.focused_window().copied().unwrap_or_default().hwnd {
@@ -283,13 +283,13 @@ impl Stackbar {
);
}
ReleaseDC(Option::from(self.hwnd()), hdc);
ReleaseDC(self.hwnd(), hdc);
// TODO: error handling
let _ = DeleteObject(hpen.into());
let _ = DeleteObject(hpen);
// TODO: error handling
let _ = DeleteObject(hbrush.into());
let _ = DeleteObject(hbrush);
// TODO: error handling
let _ = DeleteObject(hfont.into());
let _ = DeleteObject(hfont);
}
Ok(())

View File

@@ -1,22 +1,13 @@
use crate::animation::PerAnimationPrefixConfig;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::animation::DEFAULT_ANIMATION_FPS;
use crate::border_manager;
use crate::border_manager::ZOrder;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::colour::Colour;
use crate::core::BorderImplementation;
use crate::core::StackbarLabel;
use crate::core::StackbarMode;
use crate::current_virtual_desktop;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
use crate::ring::Ring;
@@ -36,24 +27,22 @@ 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::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
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;
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;
@@ -102,26 +91,21 @@ use std::sync::Arc;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BorderColours {
/// Border colour when the container contains a single window
#[serde(skip_serializing_if = "Option::is_none")]
pub single: Option<Colour>,
/// Border colour when the container contains multiple windows
#[serde(skip_serializing_if = "Option::is_none")]
pub stack: Option<Colour>,
/// Border colour when the container is in monocle mode
#[serde(skip_serializing_if = "Option::is_none")]
pub monocle: Option<Colour>,
/// Border colour when the container is in floating mode
#[serde(skip_serializing_if = "Option::is_none")]
pub floating: Option<Colour>,
/// Border colour when the container is unfocused
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused: Option<Colour>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceConfig {
/// Name
pub name: String,
@@ -131,7 +115,7 @@ pub struct WorkspaceConfig {
/// END OF LIFE FEATURE: Custom Layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_layout: Option<PathBuf>,
/// Layout rules in the format of threshold => layout (default: None)
/// Layout rules (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
/// END OF LIFE FEATURE: Custom layout rules (default: None)
@@ -155,15 +139,10 @@ pub struct WorkspaceConfig {
/// Determine what happens when a new window is opened (default: Create)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_container_behaviour: Option<WindowContainerBehaviour>,
/// Window container behaviour rules in the format of threshold => behaviour (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_container_behaviour_rules: Option<HashMap<usize, WindowContainerBehaviour>>,
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
/// Enable or disable float override, which makes it so every new window opens in floating mode
/// (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_override: Option<bool>,
/// Specify an axis on which to flip the selected layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_flip: Option<Axis>,
}
impl From<&Workspace> for WorkspaceConfig {
@@ -177,12 +156,6 @@ impl From<&Workspace> for WorkspaceConfig {
Layout::Custom(_) => {}
}
}
let layout_rules = (!layout_rules.is_empty()).then_some(layout_rules);
let mut window_container_behaviour_rules = HashMap::new();
for (threshold, behaviour) in value.window_container_behaviour_rules().iter().flatten() {
window_container_behaviour_rules.insert(*threshold, *behaviour);
}
let default_container_padding = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
let default_workspace_padding = DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst);
@@ -208,42 +181,27 @@ impl From<&Workspace> for WorkspaceConfig {
.name()
.clone()
.unwrap_or_else(|| String::from("unnamed")),
layout: value
.tile()
.then_some(match value.layout() {
Layout::Default(layout) => Option::from(*layout),
Layout::Custom(_) => None,
})
.flatten(),
custom_layout: value
.workspace_config()
.as_ref()
.and_then(|c| c.custom_layout.clone()),
layout_rules,
custom_layout_rules: value
.workspace_config()
.as_ref()
.and_then(|c| c.custom_layout_rules.clone()),
layout: match value.layout() {
Layout::Default(layout) => Option::from(*layout),
// TODO: figure out how we might resolve file references in the future
Layout::Custom(_) => None,
},
custom_layout: None,
layout_rules: Option::from(layout_rules),
// TODO: figure out how we might resolve file references in the future
custom_layout_rules: None,
container_padding,
workspace_padding,
initial_workspace_rules: value
.workspace_config()
.as_ref()
.and_then(|c| c.initial_workspace_rules.clone()),
workspace_rules: value
.workspace_config()
.as_ref()
.and_then(|c| c.workspace_rules.clone()),
initial_workspace_rules: None,
workspace_rules: None,
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
window_container_behaviour: *value.window_container_behaviour(),
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
float_override: *value.float_override(),
layout_flip: value.layout_flip(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Workspace configurations
pub workspaces: Vec<WorkspaceConfig>,
@@ -274,8 +232,8 @@ impl From<&Monitor> for MonitorConfig {
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
/// The `komorebi.json` static configuration file reference for `v0.1.35`
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.31`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -305,7 +263,7 @@ pub struct StaticConfig {
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
#[serde(skip_serializing_if = "Option::is_none")]
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
/// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead
/// END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
/// Enable or disable mouse follows focus (default: true)
@@ -334,7 +292,7 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_style")]
pub border_style: Option<BorderStyle>,
/// DEPRECATED from v0.1.31: no longer required
/// Active window border z-order (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>,
/// Active window border implementation (default: Komorebi)
@@ -386,9 +344,6 @@ 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>>,
@@ -411,33 +366,23 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub slow_application_compensation_time: Option<u64>,
/// Komorebi status bar configuration files for multiple instances on different monitors
#[serde(skip_serializing_if = "Option::is_none")]
// this option is a little special because it is only consumed by komorebic
#[serde(skip_serializing_if = "Option::is_none")]
pub bar_configurations: Option<Vec<PathBuf>>,
/// 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(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct AnimationsConfig {
/// Enable or disable animations (default: false)
pub enabled: PerAnimationPrefixConfig<bool>,
enabled: bool,
/// Set the animation duration in ms (default: 250)
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<PerAnimationPrefixConfig<u64>>,
duration: Option<u64>,
/// Set the animation style (default: Linear)
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
style: Option<AnimationStyle>,
/// Set the animation FPS (default: 60)
#[serde(skip_serializing_if = "Option::is_none")]
pub fps: Option<u64>,
fps: Option<u64>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "palette")]
pub enum KomorebiTheme {
/// A theme from catppuccin-egui
@@ -445,63 +390,45 @@ pub enum KomorebiTheme {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin,
/// Border colour when the container contains a single window (default: Blue)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container contains multiple windows (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is in monocle mode (default: Pink)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the window is floating (default: Yellow)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused (default: Base)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar focused tab text colour (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar unfocused tab text colour (default: Text)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar tab background colour (default: Base)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::CatppuccinValue>,
/// Komorebi status bar accent (default: Blue)
#[serde(skip_serializing_if = "Option::is_none")]
bar_accent: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
name: komorebi_themes::Base16,
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
bar_accent: Option<komorebi_themes::Base16Value>,
},
}
@@ -565,7 +492,7 @@ impl StaticConfig {
}
pub fn deprecated(raw: &str) {
let deprecated_options = ["invisible_borders", "border_z_order"];
let deprecated_options = ["invisible_borders"];
let deprecated_variants = vec![
("Hide", "window_hiding_behaviour", "Cloak"),
("Minimize", "window_hiding_behaviour", "Cloak"),
@@ -587,41 +514,31 @@ impl StaticConfig {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TabsConfig {
/// Width of a stackbar tab
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<i32>,
width: Option<i32>,
/// Focused tab text colour
#[serde(skip_serializing_if = "Option::is_none")]
pub focused_text: Option<Colour>,
focused_text: Option<Colour>,
/// Unfocused tab text colour
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused_text: Option<Colour>,
unfocused_text: Option<Colour>,
/// Tab background colour
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<Colour>,
background: Option<Colour>,
/// Font family
#[serde(skip_serializing_if = "Option::is_none")]
pub font_family: Option<String>,
font_family: Option<String>,
/// Font size
#[serde(skip_serializing_if = "Option::is_none")]
pub font_size: Option<i32>,
font_size: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StackbarConfig {
/// Stackbar height
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<i32>,
/// Stackbar label
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<StackbarLabel>,
/// Stackbar mode
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<StackbarMode>,
/// Stackbar tab configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub tabs: Option<TabsConfig>,
}
@@ -678,7 +595,7 @@ impl From<&WindowManager> for StaticConfig {
),
transparency_ignore_rules: None,
border_style: Option::from(STYLE.load()),
border_z_order: None,
border_z_order: Option::from(Z_ORDER.load()),
border_implementation: Option::from(IMPLEMENTATION.load()),
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
@@ -695,17 +612,7 @@ impl From<&WindowManager> for StaticConfig {
border_overflow_applications: None,
tray_and_multi_window_applications: None,
layered_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<_>>(),
),
object_name_change_applications: None,
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
stackbar: None,
@@ -716,8 +623,6 @@ 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()),
}
}
}
@@ -725,10 +630,6 @@ 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);
@@ -753,43 +654,11 @@ impl StaticConfig {
}
if let Some(animations) = &self.animation {
match &animations.enabled {
PerAnimationPrefixConfig::Prefix(enabled) => {
ANIMATION_ENABLED_PER_ANIMATION.lock().clone_from(enabled);
}
PerAnimationPrefixConfig::Global(enabled) => {
ANIMATION_ENABLED_GLOBAL.store(*enabled, Ordering::SeqCst);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
}
}
match &animations.style {
Some(PerAnimationPrefixConfig::Prefix(style)) => {
ANIMATION_STYLE_PER_ANIMATION.lock().clone_from(style);
}
Some(PerAnimationPrefixConfig::Global(style)) => {
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
*animation_style = *style;
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
}
None => {}
}
match &animations.duration {
Some(PerAnimationPrefixConfig::Prefix(duration)) => {
ANIMATION_DURATION_PER_ANIMATION.lock().clone_from(duration);
}
Some(PerAnimationPrefixConfig::Global(duration)) => {
ANIMATION_DURATION_GLOBAL.store(*duration, Ordering::SeqCst);
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
}
None => {}
}
ANIMATION_FPS.store(
animations.fps.unwrap_or(DEFAULT_ANIMATION_FPS),
Ordering::SeqCst,
);
ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst);
ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst);
ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst);
let mut animation_style = ANIMATION_STYLE.lock();
*animation_style = animations.style.unwrap_or(AnimationStyle::Linear);
}
if let Some(container) = self.default_container_padding {
@@ -864,12 +733,10 @@ 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();
let mut floating_applications = FLOATING_APPLICATIONS.lock();
let mut no_titlebar_applications = NO_TITLEBAR.lock();
if let Some(rules) = &mut self.ignore_rules {
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
@@ -891,17 +758,6 @@ 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)?;
}
@@ -926,10 +782,6 @@ impl StaticConfig {
)?;
}
if let Some(rules) = &mut self.remove_titlebar_applications {
populate_rules(rules, &mut no_titlebar_applications, &mut regex_identifiers)?;
}
if let Some(stackbar) = &self.stackbar {
if let Some(height) = &stackbar.height {
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
@@ -1111,10 +963,6 @@ 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)?;
@@ -1179,7 +1027,6 @@ impl StaticConfig {
let mut wm = WindowManager {
monitors: Ring::default(),
monitor_usr_idx_map: HashMap::new(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
@@ -1208,7 +1055,6 @@ impl StaticConfig {
pending_move_op: Arc::new(None),
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
uncloack_to_ignore: 0,
known_hwnds: HashMap::new(),
};
match value.focus_follows_mouse {
@@ -1242,78 +1088,32 @@ impl StaticConfig {
let value = Self::read(path)?;
let mut wm = wm.lock();
let configs_with_preference: Vec<_> =
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
let mut configs_used = Vec::new();
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_matching_rules.clear();
drop(workspace_matching_rules);
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
(monitor
.serial_number_id()
.as_ref()
.is_some_and(|sn| sn == id)
|| monitor.device_id() == id)
.then_some(*c_idx)
});
c_idx
};
let idx = preferred_config_idx.or({
// Monitor without preferred config idx.
// Get index of first config that is not a preferred config of some other monitor
// and that has not been used yet. This might return `None` as well, in that case
// this monitor won't have a config tied to it and will use the default values.
let m_config_count = value
.monitors
.as_ref()
.map(|ms| ms.len())
.unwrap_or_default();
(0..m_config_count)
.find(|i| !configs_with_preference.contains(i) && !configs_used.contains(i))
});
if let Some(monitor_config) = value
.monitors
.as_ref()
.and_then(|ms| idx.and_then(|i| ms.get(i)))
{
if let Some(used_config_idx) = idx {
configs_used.push(used_config_idx);
}
monitor.ensure_workspace_count(monitor_config.workspaces.len());
monitor.set_work_area_offset(monitor_config.work_area_offset);
monitor.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
monitor.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
if let Some(monitors) = value.monitors {
for (i, monitor) in monitors.iter().enumerate() {
{
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
if let Some(device_id) = display_index_preferences.get(&i) {
monitor_reconciliator::insert_in_monitor_cache(device_id, monitor.clone());
}
}
// Check if this monitor config is the preferred config for this monitor and store
// a copy of the monitor itself on the monitor cache if it is.
if idx == preferred_config_idx {
let id = monitor
.serial_number_id()
.as_ref()
.map_or(monitor.device_id(), |sn| sn);
monitor_reconciliator::insert_in_monitor_cache(id, monitor.clone());
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
m.set_work_area_offset(monitor.work_area_offset);
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
m.set_window_based_work_area_offset_limit(
monitor.window_based_work_area_offset_limit.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
}
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
for (j, ws) in monitor_config.workspaces.iter().enumerate() {
for (j, ws) in monitor.workspaces.iter().enumerate() {
if let Some(rules) = &ws.workspace_rules {
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
@@ -1339,56 +1139,6 @@ impl StaticConfig {
}
}
// Check for configs that should be tied to a specific display that isn't loaded right now
// and cache a monitor with those configs with the specific `serial_number_id` so that when
// those devices are connected later we can use the correct config from the cache.
if configs_with_preference.len() > configs_used.len() {
for i in configs_with_preference
.iter()
.filter(|i| !configs_used.contains(i))
{
let id = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
display_index_preferences.get(i).cloned()
};
if let (Some(id), Some(monitor_config)) =
(id, value.monitors.as_ref().and_then(|ms| ms.get(*i)))
{
// The name, device, device_id and serial_number_id can be empty here since
// once the monitor with this preferred index actually connects the
// `load_monitor_information` function will update these fields.
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"".into(),
"".into(),
"".into(),
None,
);
m.ensure_workspace_count(monitor_config.workspaces.len());
m.set_work_area_offset(monitor_config.work_area_offset);
m.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
monitor_reconciliator::insert_in_monitor_cache(&id, m);
}
}
}
wm.enforce_workspace_rules()?;
if value.border == Some(true) {
@@ -1403,80 +1153,28 @@ impl StaticConfig {
value.apply_globals()?;
let configs_with_preference: Vec<_> =
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
let mut configs_used = Vec::new();
if let Some(monitors) = value.monitors {
for (i, monitor) in monitors.iter().enumerate() {
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
if m.work_area_offset().is_none() {
m.set_work_area_offset(monitor.work_area_offset);
}
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
m.set_window_based_work_area_offset_limit(
monitor.window_based_work_area_offset_limit.unwrap_or(1),
);
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_matching_rules.clear();
drop(workspace_matching_rules);
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
(monitor
.serial_number_id()
.as_ref()
.is_some_and(|sn| sn == id)
|| monitor.device_id() == id)
.then_some(*c_idx)
});
c_idx
};
let idx = preferred_config_idx.or({
// Monitor without preferred config idx.
// Get index of first config that is not a preferred config of some other monitor
// and that has not been used yet. This might return `None` as well, in that case
// this monitor won't have a config tied to it and will use the default values.
let m_config_count = value
.monitors
.as_ref()
.map(|ms| ms.len())
.unwrap_or_default();
(0..m_config_count)
.find(|i| !configs_with_preference.contains(i) && !configs_used.contains(i))
});
if let Some(monitor_config) = value
.monitors
.as_ref()
.and_then(|ms| idx.and_then(|i| ms.get(i)))
{
if let Some(used_config_idx) = idx {
configs_used.push(used_config_idx);
}
monitor.ensure_workspace_count(monitor_config.workspaces.len());
if monitor.work_area_offset().is_none() {
monitor.set_work_area_offset(monitor_config.work_area_offset);
}
monitor.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
monitor.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
}
// Check if this monitor config is the preferred config for this monitor and store
// a copy of the monitor itself on the monitor cache if it is.
if idx == preferred_config_idx {
let id = monitor
.serial_number_id()
.as_ref()
.map_or(monitor.device_id(), |sn| sn);
monitor_reconciliator::insert_in_monitor_cache(id, monitor.clone());
}
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
for (j, ws) in monitor_config.workspaces.iter().enumerate() {
workspace_matching_rules.clear();
for (j, ws) in monitor.workspaces.iter().enumerate() {
if let Some(rules) = &ws.workspace_rules {
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
@@ -1502,56 +1200,6 @@ impl StaticConfig {
}
}
// Check for configs that should be tied to a specific display that isn't loaded right now
// and cache a monitor with those configs with the specific `serial_number_id` so that when
// those devices are connected later we can use the correct config from the cache.
if configs_with_preference.len() > configs_used.len() {
for i in configs_with_preference
.iter()
.filter(|i| !configs_used.contains(i))
{
let id = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
display_index_preferences.get(i).cloned()
};
if let (Some(id), Some(monitor_config)) =
(id, value.monitors.as_ref().and_then(|ms| ms.get(*i)))
{
// The name, device, device_id and serial_number_id can be empty here since
// once the monitor with this preferred index actually connects the
// `load_monitor_information` function will update these fields.
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"".into(),
"".into(),
"".into(),
None,
);
m.ensure_workspace_count(monitor_config.workspaces.len());
m.set_work_area_offset(monitor_config.work_area_offset);
m.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
monitor_reconciliator::insert_in_monitor_cache(&id, m);
}
}
}
wm.enforce_workspace_rules()?;
if let Some(enabled) = value.border {

View File

@@ -1,21 +1,11 @@
use crate::animation::lerp::Lerp;
use crate::animation::prefix::new_animation_key;
use crate::animation::prefix::AnimationPrefix;
use crate::animation::AnimationEngine;
use crate::animation::RenderDispatcher;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_MANAGER;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::border_manager;
use crate::com::SetCloak;
use crate::focus_manager;
use crate::stackbar_manager;
use crate::windows_api;
use crate::AnimationStyle;
use crate::FLOATING_WINDOW_TOGGLE_ASPECT_RATIO;
use crate::ANIMATIONS_IN_PROGRESS;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
use std::collections::HashMap;
@@ -25,7 +15,6 @@ use std::fmt::Formatter;
use std::fmt::Write as _;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use crate::core::config_generation::IdWithIdentifier;
@@ -40,14 +29,13 @@ use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use strum::Display;
use strum::EnumString;
use windows::Win32::Foundation::HWND;
use crate::core::ApplicationIdentifier;
use crate::core::HidingBehaviour;
use crate::core::Rect;
use crate::animation::Animation;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::transparency_manager;
@@ -70,11 +58,16 @@ pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
pub struct Window {
pub hwnd: isize,
#[serde(skip)]
animation: Animation,
}
impl From<isize> for Window {
fn from(value: isize) -> Self {
Self { hwnd: value }
Self {
hwnd: value,
animation: Animation::new(value),
}
}
}
@@ -82,6 +75,7 @@ impl From<HWND> for Window {
fn from(value: HWND) -> Self {
Self {
hwnd: value.0 as isize,
animation: Animation::new(value.0 as isize),
}
}
}
@@ -161,187 +155,6 @@ impl Serialize for Window {
}
}
struct MovementRenderDispatcher {
hwnd: isize,
start_rect: Rect,
target_rect: Rect,
top: bool,
style: AnimationStyle,
}
impl MovementRenderDispatcher {
const PREFIX: AnimationPrefix = AnimationPrefix::Movement;
pub fn new(
hwnd: isize,
start_rect: Rect,
target_rect: Rect,
top: bool,
style: AnimationStyle,
) -> Self {
Self {
hwnd,
start_rect,
target_rect,
top,
style,
}
}
}
impl RenderDispatcher for MovementRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> Result<()> {
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
Ok(())
}
fn render(&self, progress: f64) -> Result<()> {
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
WindowsApi::invalidate_rect(self.hwnd, None, false);
Ok(())
}
fn post_render(&self) -> Result<()> {
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top)?;
if ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX)
== 0
{
if WindowsApi::foreground_window().unwrap_or_default() == self.hwnd {
focus_manager::send_notification(self.hwnd)
}
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
Ok(())
}
}
struct TransparencyRenderDispatcher {
hwnd: isize,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
is_opaque: bool,
}
impl TransparencyRenderDispatcher {
const PREFIX: AnimationPrefix = AnimationPrefix::Transparency;
pub fn new(
hwnd: isize,
is_opaque: bool,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
) -> Self {
Self {
hwnd,
start_opacity,
target_opacity,
style,
is_opaque,
}
}
}
impl RenderDispatcher for TransparencyRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(TransparencyRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> Result<()> {
//transparent
if !self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
fn render(&self, progress: f64) -> Result<()> {
WindowsApi::set_transparent(
self.hwnd,
self.start_opacity
.lerp(self.target_opacity, progress, self.style),
)
}
fn post_render(&self) -> Result<()> {
//opaque
if self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
}
#[derive(
Copy, Clone, Debug, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
)]
#[serde(untagged)]
pub enum AspectRatio {
/// A predefined aspect ratio
Predefined(PredefinedAspectRatio),
/// A custom W:H aspect ratio
Custom(i32, i32),
}
impl Default for AspectRatio {
fn default() -> Self {
AspectRatio::Predefined(PredefinedAspectRatio::default())
}
}
#[derive(
Copy, Clone, Debug, Default, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
)]
pub enum PredefinedAspectRatio {
/// 21:9
Ultrawide,
/// 16:9
Widescreen,
/// 4:3
#[default]
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))
@@ -359,82 +172,81 @@ impl Window {
let corrected_relative_y = (window_relative_y as f32 * y_ratio) as i32;
let window_x = current_area.left + corrected_relative_x;
let window_y = current_area.top + corrected_relative_y;
let left = x_diff + window_x;
let top = y_diff + window_y;
let corrected_width = (current_rect.right as f32 * x_ratio) as i32;
let corrected_height = (current_rect.bottom as f32 * y_ratio) as i32;
let new_rect = Rect {
left,
top,
right: corrected_width,
bottom: corrected_height,
left: x_diff + window_x,
top: y_diff + window_y,
right: current_rect.right,
bottom: current_rect.bottom,
};
//TODO: We might need to take into account the differences in DPI for the new_rect, unless
//we can use the xy ratios above to the right/bottom (width/height of window) as well?
let is_maximized = &new_rect == target_area;
if is_maximized {
windows_api::WindowsApi::unmaximize_window(self.hwnd);
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled
.get(&MovementRenderDispatcher::PREFIX)
.is_some_and(|v| *v);
drop(animation_enabled);
if move_enabled || ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst) {
let anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
self.set_position(&new_rect, true)?;
let hwnd = self.hwnd;
// Wait for the animation to finish before maximizing the window again, otherwise
// we would be maximizing the window on the current monitor anyway
thread::spawn(move || {
let mut new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
let mut max_wait = 2000; // Max waiting time. No one will be using an animation longer than 2s, right? RIGHT??? WHY?
while new_anim_count > anim_count && max_wait > 0 {
thread::sleep(Duration::from_millis(10));
new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
max_wait -= 1;
}
windows_api::WindowsApi::maximize_window(hwnd);
});
} else {
self.set_position(&new_rect, true)?;
windows_api::WindowsApi::maximize_window(self.hwnd);
}
} else {
self.set_position(&new_rect, true)?;
}
Ok(())
self.set_position(&new_rect, true)
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
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);
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
self.set_position(
&Rect {
left: x,
top: y,
right: target_width,
bottom: target_height,
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,
},
true,
)
}
pub fn animate_position(&self, start_rect: &Rect, target_rect: &Rect, top: bool) -> Result<()> {
let start_rect = *start_rect;
let target_rect = *target_rect;
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
let mut animation = self.animation;
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
border_manager::send_notification(Some(self.hwnd));
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
let hwnd = self.hwnd;
std::thread::spawn(move || {
animation.animate(duration, |progress: f64| {
let new_rect = Animation::lerp_rect(&start_rect, &target_rect, progress);
if progress == 1.0 {
WindowsApi::position_window(hwnd, &new_rect, top)?;
if WindowsApi::foreground_window().unwrap_or_default() == hwnd {
focus_manager::send_notification(hwnd)
}
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
.store(false, Ordering::SeqCst);
border_manager::send_notification(Some(hwnd));
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
} else {
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(hwnd, &new_rect, false)?;
WindowsApi::invalidate_rect(hwnd, None, false);
}
Ok(())
})
});
Ok(())
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
let window_rect = WindowsApi::window_rect(self.hwnd)?;
@@ -442,27 +254,8 @@ impl Window {
return Ok(());
}
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled.get(&MovementRenderDispatcher::PREFIX);
if move_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&MovementRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&MovementRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher =
MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style);
AnimationEngine::animate(render_dispatcher, duration)
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
self.animate_position(&window_rect, layout, top)
} else {
WindowsApi::position_window(self.hwnd, layout, top)
}
@@ -570,81 +363,20 @@ impl Window {
Ok(())
}
pub fn is_focused(self) -> bool {
WindowsApi::foreground_window().unwrap_or_default() == self.hwnd
}
pub fn transparent(self) -> Result<()> {
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
false,
WindowsApi::get_transparent(self.hwnd).unwrap_or(255),
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
}
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
}
pub fn opaque(self) -> Result<()> {
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
true,
WindowsApi::get_transparent(self.hwnd)
.unwrap_or(transparency_manager::TRANSPARENCY_ALPHA.load_consume()),
255,
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)
}
let mut ex_style = self.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)
}
pub fn set_accent(self, colour: u32) -> Result<()> {
@@ -836,7 +568,7 @@ pub struct RuleDebug {
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_floating_applications: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
pub matches_no_titlebar: Option<MatchingRule>,
pub matches_no_titlebar: Option<String>,
}
#[allow(clippy::too_many_arguments)]
@@ -924,11 +656,7 @@ fn window_is_eligible(
let known_layered_hwnds = transparency_manager::known_hwnds();
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(_, _)))
{
allow_layered = if known_layered_hwnds.contains(&hwnd) {
debug.allow_layered_transparency = true;
true
} else {
@@ -945,19 +673,9 @@ fn window_is_eligible(
allow
};
let titlebars_removed = NO_TITLEBAR.lock();
let allow_titlebar_removed = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&titlebars_removed,
&regex_identifiers,
) {
debug.matches_no_titlebar = Some(rule);
true
} else {
false
let allow_titlebar_removed = {
let titlebars_removed = NO_TITLEBAR.lock();
titlebars_removed.contains(exe_name)
};
{
@@ -980,12 +698,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;

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@ 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)]
@@ -177,8 +176,6 @@ 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()?;
@@ -186,7 +183,7 @@ impl WindowManagerEvent {
let class = &window.class().ok()?;
let path = &window.path().ok()?;
let mut should_trigger_show = should_act(
let should_trigger_show = should_act(
title,
exe_name,
class,
@@ -196,14 +193,6 @@ 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

@@ -1,7 +1,6 @@
use core::ffi::c_void;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::ffi::c_void;
use std::mem::size_of;
use color_eyre::eyre::anyhow;
@@ -12,6 +11,7 @@ use windows::core::Result as WindowsCrateResult;
use windows::core::PCWSTR;
use windows::core::PWSTR;
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Foundation::HINSTANCE;
@@ -48,8 +48,6 @@ use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Power::RegisterPowerSettingNotification;
use windows::Win32::System::Power::HPOWERNOTIFY;
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
use windows::Win32::System::RemoteDesktop::WTSRegisterSessionNotification;
use windows::Win32::System::Threading::GetCurrentProcessId;
@@ -79,7 +77,6 @@ use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetLayeredWindowAttributes;
use windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
@@ -94,7 +91,6 @@ use windows::Win32::UI::WindowsAndMessaging::MoveWindow;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
use windows::Win32::UI::WindowsAndMessaging::RegisterDeviceNotificationW;
use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
@@ -104,14 +100,12 @@ use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HDEVNOTIFY;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
@@ -134,12 +128,12 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use windows_core::BOOL;
use crate::core::Rect;
@@ -150,7 +144,6 @@ use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::Window;
use crate::WindowManager;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
@@ -160,7 +153,6 @@ macro_rules! as_ptr {
};
}
use crate::border_manager::Border;
pub(crate) use as_ptr;
pub enum WindowsResult<T, E> {
@@ -236,9 +228,16 @@ impl WindowsApi {
callback: MONITORENUMPROC,
callback_data_address: isize,
) -> Result<()> {
unsafe { EnumDisplayMonitors(None, None, callback, LPARAM(callback_data_address)) }
.ok()
.process()
unsafe {
EnumDisplayMonitors(
HDC(std::ptr::null_mut()),
None,
callback,
LPARAM(callback_data_address),
)
}
.ok()
.process()
}
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
@@ -253,10 +252,7 @@ impl WindowsApi {
.collect::<Vec<_>>())
}
pub fn load_monitor_information(wm: &mut WindowManager) -> Result<()> {
let monitors = &mut wm.monitors;
let monitor_usr_idx_map = &mut wm.monitor_usr_idx_map;
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
'read: for display in win32_display_data::connected_displays_all().flatten() {
let path = display.device_path.clone();
@@ -287,7 +283,6 @@ impl WindowsApi {
name,
device,
device_id,
display.serial_number_id,
);
let mut index_preference = None;
@@ -300,8 +295,7 @@ impl WindowsApi {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, id) in &*display_index_preferences {
if m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || id.eq(m.device_id())
{
if id.eq(m.device_id()) {
index_preference = Option::from(index);
}
}
@@ -330,40 +324,6 @@ impl WindowsApi {
.elements_mut()
.retain(|m| m.name().ne("PLACEHOLDER"));
// Rebuild monitor index map
*monitor_usr_idx_map = HashMap::new();
let mut added_monitor_idxs = Vec::new();
for (index, id) in &*DISPLAY_INDEX_PREFERENCES.lock() {
if let Some(m_idx) = monitors.elements().iter().position(|m| {
m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || m.device_id() == id
}) {
monitor_usr_idx_map.insert(*index, m_idx);
added_monitor_idxs.push(m_idx);
}
}
let max_usr_idx = monitors
.elements()
.len()
.max(monitor_usr_idx_map.keys().max().map_or(0, |v| *v));
let mut available_usr_idxs = (0..max_usr_idx)
.filter(|i| !monitor_usr_idx_map.contains_key(i))
.collect::<Vec<_>>();
let not_added_monitor_idxs = (0..monitors.elements().len())
.filter(|i| !added_monitor_idxs.contains(i))
.collect::<Vec<_>>();
for i in not_added_monitor_idxs {
if let Some(next_usr_idx) = available_usr_idxs.first() {
monitor_usr_idx_map.insert(*next_usr_idx, i);
available_usr_idxs.remove(0);
} else if let Some(idx) = monitor_usr_idx_map.keys().max() {
monitor_usr_idx_map.insert(*idx, i);
}
}
Ok(())
}
@@ -492,13 +452,7 @@ impl WindowsApi {
}
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
let flags = {
SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_REDRAW
| SetWindowPosition::SHOW_WINDOW
};
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
Self::set_window_pos(
HWND(as_ptr!(hwnd)),
layout,
@@ -512,7 +466,7 @@ impl WindowsApi {
unsafe {
SetWindowPos(
hwnd,
Option::from(position),
position,
layout.left,
layout.top,
layout.right,
@@ -550,7 +504,7 @@ impl WindowsApi {
}
fn post_message(hwnd: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> Result<()> {
unsafe { PostMessageW(Option::from(hwnd), message, wparam, lparam) }.process()
unsafe { PostMessageW(hwnd, message, wparam, lparam) }.process()
}
pub fn close_window(hwnd: isize) -> Result<()> {
@@ -593,7 +547,7 @@ impl WindowsApi {
// Error ignored, as the operation is not always necessary.
let _ = SetWindowPos(
HWND(as_ptr!(hwnd)),
Option::from(HWND_TOP),
HWND_TOP,
0,
0,
0,
@@ -609,7 +563,7 @@ impl WindowsApi {
#[allow(dead_code)]
pub fn top_window() -> Result<isize> {
unsafe { GetTopWindow(None)? }.process()
unsafe { GetTopWindow(HWND::default())? }.process()
}
pub fn desktop_window() -> Result<isize> {
@@ -721,7 +675,12 @@ impl WindowsApi {
}
pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> {
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
Self::place_cursor_at_top_left_of_rect(rect)
// Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
}
pub fn place_cursor_at_top_left_of_rect(rect: &Rect) -> Result<()> {
Self::set_cursor_pos(rect.left + 15, rect.top + 15)
}
pub fn window_thread_process_id(hwnd: isize) -> (u32, u32) {
@@ -925,7 +884,7 @@ impl WindowsApi {
}
pub fn is_window(hwnd: isize) -> bool {
unsafe { IsWindow(Option::from(HWND(as_ptr!(hwnd)))) }.into()
unsafe { IsWindow(HWND(as_ptr!(hwnd))) }.into()
}
pub fn is_window_visible(hwnd: isize) -> bool {
@@ -976,7 +935,6 @@ impl WindowsApi {
name,
device,
device_id,
display.serial_number_id,
);
return Ok(monitor);
@@ -1136,14 +1094,10 @@ impl WindowsApi {
.process()
}
pub fn create_border_window(
name: PCWSTR,
instance: isize,
border: *const Border,
) -> Result<isize> {
pub fn create_border_window(name: PCWSTR, instance: isize) -> Result<isize> {
unsafe {
CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
name,
name,
WS_POPUP | WS_SYSMENU,
@@ -1153,9 +1107,13 @@ impl WindowsApi {
CW_USEDEFAULT,
None,
None,
Option::from(HINSTANCE(as_ptr!(instance))),
Some(border as _),
)?
HINSTANCE(as_ptr!(instance)),
None,
)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd
}
.process()
}
@@ -1174,21 +1132,6 @@ impl WindowsApi {
Ok(())
}
pub fn get_transparent(hwnd: isize) -> Result<u8> {
unsafe {
let mut alpha: u8 = u8::default();
let mut color_ref = COLORREF(-1i32 as u32);
let mut flags = LWA_ALPHA;
GetLayeredWindowAttributes(
HWND(as_ptr!(hwnd)),
Some(std::ptr::addr_of_mut!(color_ref)),
Some(std::ptr::addr_of_mut!(alpha)),
Some(std::ptr::addr_of_mut!(flags)),
)?;
Ok(alpha)
}
}
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> Result<isize> {
unsafe {
CreateWindowExW(
@@ -1202,35 +1145,16 @@ impl WindowsApi {
CW_USEDEFAULT,
None,
None,
Option::from(HINSTANCE(as_ptr!(instance))),
HINSTANCE(as_ptr!(instance)),
None,
)?
}
.process()
}
pub fn register_power_setting_notification(
hwnd: isize,
guid: &windows_core::GUID,
flags: REGISTER_NOTIFICATION_FLAGS,
) -> WindowsCrateResult<HPOWERNOTIFY> {
unsafe { RegisterPowerSettingNotification(HANDLE::from(HWND(as_ptr!(hwnd))), guid, flags) }
}
pub fn register_device_notification(
hwnd: isize,
mut filter: DEV_BROADCAST_DEVICEINTERFACE_W,
flags: REGISTER_NOTIFICATION_FLAGS,
) -> WindowsCrateResult<HDEVNOTIFY> {
unsafe {
let state_ptr: *const c_void = &mut filter as *mut _ as *const c_void;
RegisterDeviceNotificationW(HANDLE::from(HWND(as_ptr!(hwnd))), state_ptr, flags)
}
}
pub fn invalidate_rect(hwnd: isize, rect: Option<&Rect>, erase: bool) -> bool {
let rect = rect.map(|rect| &rect.rect() as *const RECT);
unsafe { InvalidateRect(Option::from(HWND(as_ptr!(hwnd))), rect, erase) }.as_bool()
unsafe { InvalidateRect(HWND(as_ptr!(hwnd)), rect, erase) }.as_bool()
}
pub fn alt_is_pressed() -> bool {

View File

@@ -1,6 +1,10 @@
use std::collections::VecDeque;
use crate::border_manager;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use crate::container::Container;
use crate::window::RuleDebug;
use crate::window::Window;
@@ -8,19 +12,6 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongW;
use windows::Win32::UI::WindowsAndMessaging::SendNotifyMessageW;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::OBJID_WINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows_core::BOOL;
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
@@ -69,15 +60,6 @@ pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
fn has_filtered_style(hwnd: HWND) -> bool {
let style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) as u32 };
let ex_style = unsafe { GetWindowLongW(hwnd, GWL_EXSTYLE) as u32 };
style & WS_CHILD.0 != 0
|| ex_style & WS_EX_TOOLWINDOW.0 != 0
|| ex_style & WS_EX_NOACTIVATE.0 != 0
}
pub extern "system" fn win_event_hook(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
@@ -87,7 +69,8 @@ pub extern "system" fn win_event_hook(
_id_event_thread: u32,
_dwms_event_time: u32,
) {
if id_object != OBJID_WINDOW.0 {
// OBJID_WINDOW
if id_object != 0 {
return;
}
@@ -98,33 +81,6 @@ pub extern "system" fn win_event_hook(
Err(_) => return,
};
// this forwards the message to the window's border when it moves or is destroyed
// see border_manager/border.rs
if matches!(
winevent,
WinEvent::ObjectLocationChange | WinEvent::ObjectDestroy
) && !has_filtered_style(hwnd)
{
let border_window = border_manager::window_border(hwnd.0 as isize);
if let Some(border) = border_window {
unsafe {
let _ =
SendNotifyMessageW(border.hwnd(), event, WPARAM(0), LPARAM(hwnd.0 as isize));
}
}
}
// sometimes the border focus state and colors don't get updated because this event comes too
// slow for the value of GetForegroundWindow to be up to date by the time it is inspected in
// the border manager to determine if a window show have its border show as "focused"
//
// so here we can just fire another event at the border manager when the system has finally
// registered the new foreground window and this time the correct border colors will be applied
if matches!(winevent, WinEvent::SystemForeground) && !has_filtered_style(hwnd) {
border_manager::send_notification(Some(hwnd.0 as isize));
}
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => {
tracing::trace!(

View File

@@ -3,6 +3,7 @@ use std::time::Duration;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Accessibility::SetWinEventHook;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
@@ -10,8 +11,6 @@ use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WINEVENT_OUTOFCONTEXT;
use windows::Win32::UI::WindowsAndMessaging::WINEVENT_SKIPOWNPROCESS;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_callbacks;
@@ -32,7 +31,7 @@ pub fn start() {
Some(windows_callbacks::win_event_hook),
0,
0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS,
0,
)
};
@@ -40,7 +39,7 @@ pub fn start() {
loop {
unsafe {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
if !GetMessageW(&mut msg, HWND(std::ptr::null_mut()), 0, 0).as_bool() {
tracing::debug!("windows event processing thread shutdown");
break;
};

View File

@@ -1,6 +1,4 @@
use std::collections::VecDeque;
use std::fmt::Display;
use std::fmt::Formatter;
use std::num::NonZeroUsize;
use std::sync::atomic::Ordering;
@@ -26,7 +24,6 @@ use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::container::Container;
use crate::ring::Ring;
use crate::should_act;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::static_config::WorkspaceConfig;
@@ -38,7 +35,6 @@ use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::NO_TITLEBAR;
use crate::REGEX_IDENTIFIERS;
use crate::REMOVE_TITLEBARS;
#[allow(clippy::struct_field_names)]
@@ -56,65 +52,42 @@ use crate::REMOVE_TITLEBARS;
)]
pub struct Workspace {
#[getset(get = "pub", set = "pub")]
pub name: Option<String>,
pub containers: Ring<Container>,
name: Option<String>,
containers: Ring<Container>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub monocle_container: Option<Container>,
monocle_container: Option<Container>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
pub monocle_container_restore_idx: Option<usize>,
monocle_container_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub maximized_window: Option<Window>,
maximized_window: Option<Window>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
pub maximized_window_restore_idx: Option<usize>,
maximized_window_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub")]
pub floating_windows: Vec<Window>,
floating_windows: Vec<Window>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub layout: Layout,
layout: Layout,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub layout_rules: Vec<(usize, Layout)>,
layout_rules: Vec<(usize, Layout)>,
#[getset(get_copy = "pub", set = "pub")]
pub layout_flip: Option<Axis>,
layout_flip: Option<Axis>,
#[getset(get_copy = "pub", set = "pub")]
pub workspace_padding: Option<i32>,
workspace_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
pub container_padding: Option<i32>,
container_padding: Option<i32>,
#[getset(get = "pub", set = "pub")]
pub latest_layout: Vec<Rect>,
latest_layout: Vec<Rect>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub resize_dimensions: Vec<Option<Rect>>,
resize_dimensions: Vec<Option<Rect>>,
#[getset(get = "pub", set = "pub")]
pub tile: bool,
tile: bool,
#[getset(get_copy = "pub", set = "pub")]
pub apply_window_based_work_area_offset: bool,
apply_window_based_work_area_offset: bool,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub window_container_behaviour: Option<WindowContainerBehaviour>,
window_container_behaviour: Option<WindowContainerBehaviour>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub window_container_behaviour_rules: Option<Vec<(usize, WindowContainerBehaviour)>>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub float_override: Option<bool>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub layer: WorkspaceLayer,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
pub workspace_config: Option<WorkspaceConfig>,
}
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub enum WorkspaceLayer {
#[default]
Tiling,
Floating,
}
impl Display for WorkspaceLayer {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
WorkspaceLayer::Tiling => write!(f, "Tiling"),
WorkspaceLayer::Floating => write!(f, "Floating"),
}
}
float_override: Option<bool>,
}
impl_ring_elements!(Workspace, Container);
@@ -139,36 +112,21 @@ impl Default for Workspace {
tile: true,
apply_window_based_work_area_offset: true,
window_container_behaviour: None,
window_container_behaviour_rules: None,
float_override: None,
workspace_config: None,
layer: Default::default(),
}
}
}
#[derive(Debug)]
pub enum WorkspaceWindowLocation {
Monocle(usize), // window_idx
Maximized,
Container(usize, usize), // container_idx, window_idx
Floating(usize), // idx in floating_windows
}
impl Workspace {
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> Result<()> {
self.name = Option::from(config.name.clone());
if config.container_padding.is_some() {
self.set_container_padding(config.container_padding);
} else {
self.set_container_padding(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
}
if config.workspace_padding.is_some() {
self.set_workspace_padding(config.workspace_padding);
} else {
self.set_container_padding(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
}
if let Some(layout) = &config.layout {
@@ -186,55 +144,38 @@ impl Workspace {
self.tile = false;
}
let mut all_layout_rules = vec![];
if let Some(layout_rules) = &config.layout_rules {
let mut all_rules = vec![];
for (count, rule) in layout_rules {
all_layout_rules.push((*count, Layout::Default(*rule)));
all_rules.push((*count, Layout::Default(*rule)));
}
all_layout_rules.sort_by_key(|(i, _)| *i);
self.set_layout_rules(all_rules);
self.tile = true;
}
self.set_layout_rules(all_layout_rules.clone());
if let Some(layout_rules) = &config.custom_layout_rules {
let rules = self.layout_rules_mut();
for (count, pathbuf) in layout_rules {
let rule = CustomLayout::from_path(pathbuf)?;
all_layout_rules.push((*count, Layout::Custom(rule)));
rules.push((*count, Layout::Custom(rule)));
}
all_layout_rules.sort_by_key(|(i, _)| *i);
self.tile = true;
self.set_layout_rules(all_layout_rules);
}
self.set_apply_window_based_work_area_offset(
config.apply_window_based_work_area_offset.unwrap_or(true),
);
self.set_window_container_behaviour(config.window_container_behaviour);
if let Some(window_container_behaviour_rules) = &config.window_container_behaviour_rules {
if window_container_behaviour_rules.is_empty() {
self.set_window_container_behaviour_rules(None);
} else {
let mut all_rules = vec![];
for (count, behaviour) in window_container_behaviour_rules {
all_rules.push((*count, *behaviour));
}
all_rules.sort_by_key(|(i, _)| *i);
self.set_window_container_behaviour_rules(Some(all_rules));
}
} else {
self.set_window_container_behaviour_rules(None);
if config.window_container_behaviour.is_some() {
self.set_window_container_behaviour(config.window_container_behaviour);
}
self.set_float_override(config.float_override);
self.set_layout_flip(config.layout_flip);
self.set_workspace_config(Some(config.clone()));
if config.float_override.is_some() {
self.set_float_override(config.float_override);
}
Ok(())
}
@@ -367,28 +308,21 @@ impl Workspace {
if !self.layout_rules().is_empty() {
let mut updated_layout = None;
for (threshold, layout) in self.layout_rules() {
if self.containers().len() >= *threshold {
updated_layout = Option::from(layout.clone());
for rule in self.layout_rules() {
if self.containers().len() >= rule.0 {
updated_layout = Option::from(rule.1.clone());
}
}
if let Some(updated_layout) = updated_layout {
if !matches!(updated_layout, Layout::Default(DefaultLayout::BSP)) {
self.set_layout_flip(None);
}
self.set_layout(updated_layout);
}
}
if let Some(window_container_behaviour_rules) = self.window_container_behaviour_rules() {
let mut updated_behaviour = None;
for (threshold, behaviour) in window_container_behaviour_rules {
if self.containers().len() >= *threshold {
updated_behaviour = Option::from(*behaviour);
}
}
self.set_window_container_behaviour(updated_behaviour);
}
let managed_maximized_window = self.maximized_window().is_some();
if *self.tile() {
@@ -420,7 +354,6 @@ impl Workspace {
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
let no_titlebar = NO_TITLEBAR.lock().clone();
let regex_identifiers = REGEX_IDENTIFIERS.lock().clone();
let container_padding = self.container_padding().unwrap_or(0);
let containers = self.containers_mut();
@@ -450,19 +383,9 @@ impl Workspace {
.focused_window()
.is_some_and(|w| w.hwnd == window.hwnd)
{
let should_remove_titlebar_for_window = should_act(
&window.title().unwrap_or_default(),
&window.exe().unwrap_or_default(),
&window.class().unwrap_or_default(),
&window.path().unwrap_or_default(),
&no_titlebar,
&regex_identifiers,
)
.is_some();
if should_remove_titlebars && should_remove_titlebar_for_window {
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
window.remove_title_bar()?;
} else if should_remove_titlebar_for_window {
} else if no_titlebar.contains(&window.exe()?) {
window.add_title_bar()?;
}
@@ -485,16 +408,7 @@ impl Workspace {
// number of layouts / containers. This should never actually truncate as the remove_window
// function takes care of cleaning up resize dimensions when destroying empty containers
let container_count = self.containers().len();
// since monocle is a toggle, we never want to truncate the resize dimensions since it will
// almost always be toggled off and the container will be reintegrated into layout
//
// without this check, if there are exactly two containers, when one is toggled to monocle
// the resize dimensions will be truncated to len == 1, and when it is reintegrated, if it
// had a resize adjustment before, that will have been lost
if self.monocle_container().is_none() {
self.resize_dimensions_mut().resize(container_count, None);
}
self.resize_dimensions_mut().resize(container_count, None);
Ok(())
}
@@ -526,15 +440,7 @@ impl Workspace {
}
for window in self.visible_windows().into_iter().flatten() {
if !window.is_window()
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
// docs is closed
//
// I hate every single person who worked on Microsoft Office 365, especially Word
|| !window.is_visible()
{
if !window.is_window() {
hwnds.push(window.hwnd);
}
}
@@ -660,41 +566,6 @@ impl Workspace {
None
}
pub fn location_from_exe(&self, exe: &str) -> Option<WorkspaceWindowLocation> {
for (container_idx, container) in self.containers().iter().enumerate() {
if let Some(window_idx) = container.idx_from_exe(exe) {
return Some(WorkspaceWindowLocation::Container(
container_idx,
window_idx,
));
}
}
if let Some(window) = self.maximized_window() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Some(WorkspaceWindowLocation::Maximized);
}
}
}
if let Some(container) = self.monocle_container() {
if let Some(window_idx) = container.idx_from_exe(exe) {
return Some(WorkspaceWindowLocation::Monocle(window_idx));
}
}
for (window_idx, window) in self.floating_windows().iter().enumerate() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Some(WorkspaceWindowLocation::Floating(window_idx));
}
}
}
None
}
pub fn contains_managed_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {

View File

@@ -1,29 +0,0 @@
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();
}
}

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