Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z
67f590b5c3 feat(borders): track window movements + animations
This commit introduces a number of changes to the border manager module
to enable borders to track the movements of windows as they are being
animated.

As part of these changes, the code paths for borders to track user
movement of windows have also been overhauled.

The biggest conceptual change introduced here is borrowed from
@lukeyou05's work on tacky-borders, where the primary event listener of
the komorebi process now forwards EVENT_OBJECT_LOCATIONCHANGE and
EVENT_OBJECT_DESTROY messages from application windows directly on to
their borders.

These events are handled directly in the border window callbacks,
outside of the main border manager module event processing loop.

In order to handle these events more performantly in the border window
callbacks, a number of state trackers have been added to the Border
struct.

When handling EVENT_OBJECT_NAMECHANGE, these values are read directly
from the struct, whereas when handling WM_PAINT, which is sent by the
system whenever we invalidate a border window, we update the state
values on the Border structs from the various atomic configuration
variables in the mod.rs file.

Another trick I borrowed from tacky-borders is to store a pointer to the
Border object alongside a border window whenever it is created with
CreateWindowExW, which can be accessed within the callback as
GWLP_USERDATA.

There is some unfortunate introduction of unsafe code to make this
happen, but the callback uses null checks to exit the callback early to
ensure (to the best of my ability) that there are no pointer
dereferencing issues once we start making border changes in the context
of the callback.

There are a few other Direct2D related optimizations throughout this
commit, mainly avoiding the recreation of objects like brush properties
and brushes.

Finally, the border_z_order option is now deprecated as the border
window is now tracking the z-ordering of the application window it is
associated with by default - this should resolve a whole host of subtle
border z-ordering issues, especially when dragging windows around using
the mouse.

This work would not have been possible without the guidance of
@lukeyou05, so if you like this feature, please make sure you thank him
too!
2024-12-07 11:42:30 -08:00
69 changed files with 1350 additions and 4081 deletions

View File

@@ -1,6 +1,6 @@
name: Bug report
description: File a bug report
labels: [bug]
labels: [ bug ]
title: "[BUG]: "
body:
- type: markdown

View File

@@ -47,7 +47,7 @@ jobs:
key: ${{ matrix.platform.target }}
- run: cargo +nightly fmt --check
- run: cargo clippy
- uses: houseabsolute/actions-rust-cross@v1
- uses: houseabsolute/actions-rust-cross@v0
with:
command: "build"
target: ${{ matrix.platform.target }}
@@ -199,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 }}

717
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,8 @@ chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.30"
egui_extras = "0.30"
eframe = "0.29"
egui_extras = "0.29"
dirs = "5"
dunce = "1"
hotwatch = "0.5"
@@ -31,13 +31,13 @@ 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 = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.37"
shadow-rs = "0.35"
which = "7"
[workspace.dependencies.windows]

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.32"}
// 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,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

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

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

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,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.32/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {
@@ -33,11 +33,6 @@
}
],
"right_widgets": [
{
"Update": {
"enable": true
}
},
{
"Media": {
"enable": true
@@ -78,4 +73,4 @@
}
}
]
}
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.32/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.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:
@@ -46,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.33"
version = "0.1.31"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -16,16 +16,15 @@ 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 }

View File

@@ -38,7 +38,6 @@ use eframe::egui::Visuals;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
use komorebi_client::SocketMessage;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Catppuccin;
@@ -50,7 +49,6 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct Komobar {
pub hwnd: Option<isize>,
pub config: Arc<KomobarConfig>,
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
@@ -60,21 +58,10 @@ pub struct Komobar {
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_config: Receiver<KomobarConfig>,
pub bg_color: Rc<RefCell<Color32>>,
pub bg_color_with_alpha: Rc<RefCell<Color32>>,
pub scale_factor: f32,
pub size_rect: komorebi_client::Rect,
applied_theme_on_first_frame: bool,
}
pub fn apply_theme(
ctx: &Context,
theme: KomobarTheme,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
transparency_alpha: Option<u8>,
grouping: Option<Grouping>,
render_config: Rc<RefCell<RenderConfig>>,
) {
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
match theme {
KomobarTheme::Catppuccin {
name: catppuccin,
@@ -154,29 +141,6 @@ pub fn apply_theme(
bg_color.replace(base16.background());
}
}
// Apply transparency_alpha
let theme_color = *bg_color.borrow();
bg_color_with_alpha.replace(theme_color.try_apply_alpha(transparency_alpha));
// apply rounding to the widgets
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
&grouping
{
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.rounding = rounding.into();
style.visuals.widgets.inactive.rounding = rounding.into();
style.visuals.widgets.hovered.rounding = rounding.into();
style.visuals.widgets.active.rounding = rounding.into();
style.visuals.widgets.open.rounding = rounding.into();
});
}
}
// Update RenderConfig's background_color so that widgets will have the new color
render_config.borrow_mut().background_color = *bg_color.borrow();
}
impl Komobar {
@@ -196,136 +160,7 @@ impl Komobar {
Self::add_custom_font(ctx, font_family);
}
// Update the `size_rect` so that the bar position can be changed on the EGUI update
// function
self.update_size_rect(config.position.clone());
self.try_apply_theme(config, ctx);
if let Some(font_size) = &config.font_size {
tracing::info!("attempting to set custom font size: {font_size}");
Self::set_font_size(ctx, *font_size);
}
self.render_config.replace(config.new_renderconfig(
ctx,
*self.bg_color.borrow(),
config.icon_scale,
));
let mut komorebi_notification_state = previous_notification_state;
let mut komorebi_widgets = Vec::new();
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Left));
}
}
if let Some(center_widgets) = &config.center_widgets {
for (idx, widget_config) in center_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Center));
}
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Right));
}
}
let mut left_widgets = config
.left_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut center_widgets = match &config.center_widgets {
Some(center_widgets) => center_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>(),
None => vec![],
};
let mut right_widgets = config
.right_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if !komorebi_widgets.is_empty() {
komorebi_widgets
.into_iter()
.for_each(|(mut widget, idx, side)| {
match komorebi_notification_state {
None => {
komorebi_notification_state =
Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
if widget.workspaces.map_or(false, |w| w.enable) {
previous.borrow_mut().update_from_config(
&widget.komorebi_notification_state.borrow(),
);
}
widget.komorebi_notification_state = previous.clone();
}
}
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Alignment::Left => left_widgets[idx] = boxed,
Alignment::Center => center_widgets[idx] = boxed,
Alignment::Right => right_widgets[idx] = boxed,
}
});
}
right_widgets.reverse();
self.left_widgets = left_widgets;
self.center_widgets = center_widgets;
self.right_widgets = right_widgets;
if let (Some(prev_rect), Some(new_rect)) = (
&self.config.monitor.work_area_offset,
&config.monitor.work_area_offset,
) {
if new_rect != prev_rect {
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(config.monitor.index, *new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
config.monitor.index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
config.monitor.index
);
}
}
}
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
self.config = config.clone().into();
}
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
fn update_size_rect(&mut self, position: Option<PositionConfig>) {
let position = position.unwrap_or(PositionConfig {
let position = config.position.clone().unwrap_or(PositionConfig {
start: Some(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
@@ -336,40 +171,42 @@ impl Komobar {
}),
});
let start = position.start.unwrap_or(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
});
if let Some(hwnd) = process_hwnd() {
let start = position.start.unwrap_or(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
});
let end = position.end.unwrap_or(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
});
let end = position.end.unwrap_or(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
});
if end.y == 0.0 {
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
if end.y == 0.0 {
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
}
let rect = komorebi_client::Rect {
left: start.x as i32,
top: start.y as i32,
right: end.x as i32,
bottom: end.y as i32,
};
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
self.size_rect = komorebi_client::Rect {
left: start.x as i32,
top: start.y as i32,
right: end.x as i32,
bottom: end.y as i32,
};
}
fn try_apply_theme(&mut self, config: &KomobarConfig, ctx: &Context) {
match config.theme {
Some(theme) => {
apply_theme(
ctx,
theme,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
config.transparency_alpha,
config.grouping,
self.render_config.clone(),
);
apply_theme(ctx, theme, self.bg_color.clone());
}
None => {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
@@ -385,21 +222,11 @@ impl Komobar {
},
);
let bar_transparency_alpha = config.transparency_alpha;
let bar_grouping = config.grouping;
let config = home_dir.join("komorebi.json");
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
bar_transparency_alpha,
bar_grouping,
self.render_config.clone(),
);
apply_theme(ctx, KomobarTheme::from(theme), self.bg_color.clone());
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
@@ -420,28 +247,124 @@ impl Komobar {
Err(_) => {
ctx.set_style(Style::default());
self.bg_color.replace(Style::default().visuals.panel_fill);
// apply rounding to the widgets since we didn't call `apply_theme`
if let Some(
Grouping::Bar(config)
| Grouping::Alignment(config)
| Grouping::Widget(config),
) = &bar_grouping
{
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.rounding = rounding.into();
style.visuals.widgets.inactive.rounding = rounding.into();
style.visuals.widgets.hovered.rounding = rounding.into();
style.visuals.widgets.active.rounding = rounding.into();
style.visuals.widgets.open.rounding = rounding.into();
});
}
}
}
}
}
}
// apply rounding to the widgets
if let Some(
Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config),
) = &config.grouping
{
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.rounding = rounding.into();
style.visuals.widgets.inactive.rounding = rounding.into();
style.visuals.widgets.hovered.rounding = rounding.into();
style.visuals.widgets.active.rounding = rounding.into();
style.visuals.widgets.open.rounding = rounding.into();
});
}
}
let theme_color = *self.bg_color.borrow();
self.render_config
.replace(config.new_renderconfig(theme_color));
self.bg_color
.replace(theme_color.try_apply_alpha(config.transparency_alpha));
if let Some(font_size) = &config.font_size {
tracing::info!("attempting to set custom font size: {font_size}");
Self::set_font_size(ctx, *font_size);
}
let mut komorebi_widget = None;
let mut komorebi_widget_idx = None;
let mut komorebi_notification_state = previous_notification_state;
let mut side = None;
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Alignment::Left);
}
}
if let Some(center_widgets) = &config.center_widgets {
for (idx, widget_config) in center_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Alignment::Center);
}
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Alignment::Right);
}
}
let mut left_widgets = config
.left_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut center_widgets = match &config.center_widgets {
Some(center_widgets) => center_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>(),
None => vec![],
};
let mut right_widgets = config
.right_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if let (Some(idx), Some(mut widget), Some(side)) =
(komorebi_widget_idx, komorebi_widget, side)
{
match komorebi_notification_state {
None => {
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
previous
.borrow_mut()
.update_from_config(&widget.komorebi_notification_state.borrow());
widget.komorebi_notification_state = previous.clone();
}
}
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Alignment::Left => left_widgets[idx] = boxed,
Alignment::Center => center_widgets[idx] = boxed,
Alignment::Right => right_widgets[idx] = boxed,
}
}
right_widgets.reverse();
self.left_widgets = left_widgets;
self.center_widgets = center_widgets;
self.right_widgets = right_widgets;
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
}
pub fn new(
@@ -451,7 +374,6 @@ impl Komobar {
config: Arc<KomobarConfig>,
) -> Self {
let mut komobar = Self {
hwnd: process_hwnd(),
config: config.clone(),
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
@@ -461,10 +383,7 @@ impl Komobar {
rx_gui,
rx_config,
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
bg_color_with_alpha: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
size_rect: komorebi_client::Rect::default(),
applied_theme_on_first_frame: false,
};
komobar.apply_config(&cc.egui_ctx, &config, None);
@@ -508,7 +427,7 @@ impl Komobar {
if let Some((font, _)) = system_fonts::get(&property) {
fonts
.font_data
.insert(name.to_owned(), Arc::new(FontData::from_owned(font)));
.insert(name.to_owned(), FontData::from_owned(font));
fonts
.families
@@ -534,10 +453,6 @@ impl eframe::App for Komobar {
}
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
if self.hwnd.is_none() {
self.hwnd = process_hwnd();
}
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(
@@ -563,135 +478,71 @@ impl eframe::App for Komobar {
self.config.monitor.index,
self.rx_gui.clone(),
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme,
self.render_config.clone(),
);
}
if !self.applied_theme_on_first_frame {
self.try_apply_theme(&self.config.clone(), ctx);
self.applied_theme_on_first_frame = true;
}
// Check if egui's Window size is the expected one, if not, update it
if let Some(current_rect) = ctx.input(|i| i.viewport().outer_rect) {
// Get the correct size according to scale factor
let current_rect = komorebi_client::Rect {
left: (current_rect.min.x * self.scale_factor) as i32,
top: (current_rect.min.y * self.scale_factor) as i32,
right: ((current_rect.max.x - current_rect.min.x) * self.scale_factor) as i32,
bottom: ((current_rect.max.y - current_rect.min.y) * self.scale_factor) as i32,
};
if self.size_rect != current_rect {
if let Some(hwnd) = self.hwnd {
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&self.size_rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
}
}
let frame = if let Some(frame) = &self.config.frame {
Frame::none()
.inner_margin(Margin::symmetric(
frame.inner_margin.x,
frame.inner_margin.y,
))
.fill(*self.bg_color_with_alpha.borrow())
.fill(*self.bg_color.borrow())
} else {
Frame::none().fill(*self.bg_color_with_alpha.borrow())
Frame::none().fill(*self.bg_color.borrow())
};
let mut render_config = self.render_config.borrow_mut();
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
CentralPanel::default().frame(frame).show(ctx, |_| {
CentralPanel::default().frame(frame).show(ctx, |ui| {
// Apply grouping logic for the bar as a whole
let area_frame = if let Some(frame) = &self.config.frame {
Frame::none().inner_margin(Margin::symmetric(0.0, frame.inner_margin.y))
} else {
Frame::none()
};
render_config.clone().apply_on_bar(ui, |ui| {
ui.horizontal_centered(|ui| {
// Left-aligned widgets layout
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
let mut render_conf = *render_config;
render_conf.alignment = Some(Alignment::Left);
if !self.left_widgets.is_empty() {
// Left-aligned widgets layout
Area::new(Id::new("left_panel"))
.anchor(Align2::LEFT_CENTER, [0.0, 0.0]) // Align in the left center of the window
.show(ctx, |ui| {
let mut left_area_frame = area_frame;
if let Some(frame) = &self.config.frame {
left_area_frame.inner_margin.left = frame.inner_margin.x;
}
left_area_frame.show(ui, |ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Left);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
}
if !self.right_widgets.is_empty() {
// Right-aligned widgets layout
Area::new(Id::new("right_panel"))
.anchor(Align2::RIGHT_CENTER, [0.0, 0.0]) // Align in the right center of the window
.show(ctx, |ui| {
let mut right_area_frame = area_frame;
if let Some(frame) = &self.config.frame {
right_area_frame.inner_margin.right = frame.inner_margin.x;
}
right_area_frame.show(ui, |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Right);
// Right-aligned widgets layout
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let mut render_conf = *render_config;
render_conf.alignment = Some(Alignment::Right);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
}
if !self.center_widgets.is_empty() {
// Floating center widgets
Area::new(Id::new("center_panel"))
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
.show(ctx, |ui| {
let center_area_frame = area_frame;
center_area_frame.show(ui, |ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Center);
if !self.center_widgets.is_empty() {
// Floating center widgets
Area::new(Id::new("center_panel"))
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
.show(ctx, |ui| {
Frame::none().show(ui, |ui| {
ui.horizontal_centered(|ui| {
let mut render_conf = *render_config;
render_conf.alignment = Some(Alignment::Center);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.center_widgets {
w.render(ctx, ui, &mut render_conf);
}
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.center_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
});
});
});
});
}
}
})
})
});
}
}

View File

@@ -2,11 +2,12 @@ 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::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,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,
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(),
}
}
}
@@ -104,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,
);
@@ -117,12 +144,7 @@ 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(true, ui, |ui| {

View File

@@ -12,7 +12,7 @@ use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.33`
/// The `komorebi.bar.json` configuration file reference for `v0.1.31`
pub struct KomobarConfig {
/// Bar positioning options
#[serde(alias = "viewport")]
@@ -25,8 +25,6 @@ pub struct KomobarConfig {
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
@@ -190,16 +188,12 @@ pub enum LabelPrefix {
IconAndText,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
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,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 eframe::egui::WidgetText;
use schemars::JsonSchema;
@@ -89,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 => {
@@ -96,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,
);
@@ -108,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

@@ -2,12 +2,10 @@ use crate::bar::apply_theme;
use crate::config::DisplayFormat;
use crate::config::KomobarTheme;
use crate::komorebi_layout::KomorebiLayout;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::ICON_CACHE;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_INDEX;
use crossbeam_channel::Receiver;
@@ -16,6 +14,7 @@ use eframe::egui::vec2;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
@@ -23,6 +22,7 @@ use eframe::egui::Margin;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextStyle;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
@@ -46,7 +46,7 @@ use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: Option<KomorebiWorkspacesConfig>,
pub workspaces: KomorebiWorkspacesConfig,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Focused Window widget
@@ -113,10 +113,7 @@ impl From<&KomorebiConfig> for Komorebi {
selected_workspace: String::new(),
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
workspaces: vec![],
hide_empty_workspaces: value
.workspaces
.map(|w| w.hide_empty_workspaces)
.unwrap_or_default(),
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
@@ -134,7 +131,7 @@ impl From<&KomorebiConfig> for Komorebi {
#[derive(Clone, Debug)]
pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: Option<KomorebiWorkspacesConfig>,
pub workspaces: KomorebiWorkspacesConfig,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
@@ -143,140 +140,140 @@ pub struct Komorebi {
impl BarWidget for Komorebi {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
let icon_size = Vec2::splat(config.icon_font_id.size);
if let Some(workspaces) = self.workspaces {
if workspaces.enable {
let mut update = None;
if self.workspaces.enable {
let mut update = None;
if !komorebi_notification_state.workspaces.is_empty() {
let format = workspaces.display.unwrap_or(DisplayFormat::Text);
if !komorebi_notification_state.workspaces.is_empty() {
let format = self.workspaces.display.unwrap_or(DisplayFormat::Text);
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, container_information)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
if SelectableFrame::new(
komorebi_notification_state.selected_workspace.eq(ws),
)
.show(ui, |ui| {
let mut has_icon = false;
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, container_information)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
if SelectableFrame::new(
komorebi_notification_state.selected_workspace.eq(ws),
)
.show(ui, |ui| {
let mut has_icon = false;
if format == DisplayFormat::Icon
|| format == DisplayFormat::IconAndText
|| format == DisplayFormat::IconAndTextOnSelected
|| (format == DisplayFormat::TextAndIconOnSelected
&& komorebi_notification_state.selected_workspace.eq(ws))
{
let icons: Vec<_> =
container_information.icons.iter().flatten().collect();
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
let icons: Vec<_> =
container_information.icons.iter().flatten().collect();
if !icons.is_empty() {
Frame::none()
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y,
))
.show(ui, |ui| {
for icon in icons {
ui.add(
Image::from(&img_to_texture(ctx, icon))
.maintain_aspect_ratio(true)
.fit_to_exact_size(icon_size),
);
if !icons.is_empty() {
Frame::none()
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y,
))
.show(ui, |ui| {
for icon in icons {
ui.add(
Image::from(&img_to_texture(ctx, icon))
.maintain_aspect_ratio(true)
.shrink_to_fit(),
);
if !has_icon {
has_icon = true;
}
if !has_icon {
has_icon = true;
}
});
}
}
});
}
}
// draw a custom icon when there is no app icon
if match format {
DisplayFormat::Icon => !has_icon,
_ => false,
} {
let (response, painter) =
ui.allocate_painter(icon_size, Sense::hover());
let stroke = Stroke::new(
1.0,
ctx.style().visuals.selection.stroke.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);
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
// draw a custom icon when there is no app icon
if match format {
DisplayFormat::Icon => !has_icon,
_ => false,
} {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
response.on_hover_text(ws.to_string())
} else if match format {
DisplayFormat::Icon => has_icon,
_ => false,
} {
ui.response().on_hover_text(ws.to_string())
} else if format != DisplayFormat::IconAndTextOnSelected
|| (format == DisplayFormat::IconAndTextOnSelected
&& komorebi_notification_state.selected_workspace.eq(ws))
{
ui.add(Label::new(ws.to_string()).selectable(false))
} else {
ui.response()
}
})
.clicked()
let (response, painter) =
ui.allocate_painter(Vec2::splat(font_id.size), Sense::hover());
let stroke =
Stroke::new(1.0, ctx.style().visuals.selection.stroke.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);
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
response.on_hover_text(ws.to_string())
} else if match format {
DisplayFormat::Icon => has_icon,
_ => false,
} {
ui.response().on_hover_text(ws.to_string())
} else {
ui.add(Label::new(ws.to_string()).selectable(false))
}
})
.clicked()
{
update = Some(ws.to_string());
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
update = Some(ws.to_string());
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
if komorebi_notification_state.mouse_follows_focus {
if komorebi_client::send_batch([
SocketMessage::MouseFollowsFocus(false),
SocketMessage::FocusMonitorWorkspaceNumber(
komorebi_notification_state.monitor_index,
i,
),
SocketMessage::RetileWithResizeDimensions,
SocketMessage::MouseFollowsFocus(true),
])
.is_err()
{
tracing::error!(
"could not send the following batch of messages to komorebi:\n
MouseFollowsFocus(false)\n
FocusMonitorWorkspaceNumber({}, {})\n
RetileWithResizeDimensions
MouseFollowsFocus(true)\n",
komorebi_notification_state.monitor_index,
i,
);
}
} else if komorebi_client::send_batch([
SocketMessage::FocusMonitorWorkspaceNumber(
if proceed
&& komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
komorebi_notification_state.monitor_index,
i,
),
SocketMessage::RetileWithResizeDimensions,
])
.is_err()
{
tracing::error!(
"could not send the following batch of messages to komorebi:\n
FocusMonitorWorkspaceNumber({}, {})\n
RetileWithResizeDimensions",
komorebi_notification_state.monitor_index,
i,
);
}
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusWorkspaceNumber"
);
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
if proceed
&& komorebi_client::send_message(
&SocketMessage::RetileWithResizeDimensions,
)
.is_err()
{
tracing::error!("could not send message to komorebi: Retile");
}
}
});
}
}
});
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
}
@@ -371,10 +368,9 @@ impl BarWidget for Komorebi {
.focused_window_idx;
let iter = titles.iter().zip(icons.iter());
let len = iter.len();
for (i, (title, icon)) in iter.enumerate() {
let selected = i == focused_window_idx && len != 1;
let selected = i == focused_window_idx;
if SelectableFrame::new(selected)
.show(ui, |ui| {
@@ -387,11 +383,7 @@ impl BarWidget for Komorebi {
},
);
if format == DisplayFormat::Icon
|| format == DisplayFormat::IconAndText
|| format == DisplayFormat::IconAndTextOnSelected
|| (format == DisplayFormat::TextAndIconOnSelected
&& i == focused_window_idx)
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format
{
if let Some(img) = icon {
Frame::none()
@@ -402,7 +394,7 @@ impl BarWidget for Komorebi {
let response = ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.fit_to_exact_size(icon_size),
.shrink_to_fit(),
);
if let DisplayFormat::Icon = format {
@@ -412,11 +404,7 @@ impl BarWidget for Komorebi {
}
}
if format == DisplayFormat::Text
|| format == DisplayFormat::IconAndText
|| format == DisplayFormat::TextAndIconOnSelected
|| (format == DisplayFormat::IconAndTextOnSelected
&& i == focused_window_idx)
if let DisplayFormat::Text | DisplayFormat::IconAndText = format
{
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
@@ -436,27 +424,35 @@ impl BarWidget for Komorebi {
return;
}
if komorebi_notification_state.mouse_follows_focus {
if komorebi_client::send_batch([
SocketMessage::MouseFollowsFocus(false),
SocketMessage::FocusStackWindow(i),
SocketMessage::MouseFollowsFocus(true),
]).is_err() {
tracing::error!(
"could not send the following batch of messages to komorebi:\n
MouseFollowsFocus(false)\n
FocusStackWindow({})\n
MouseFollowsFocus(true)\n",
i,
);
}
} else if komorebi_client::send_message(
&SocketMessage::FocusStackWindow(i)
).is_err() {
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(
i,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
);
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
}
}
});
@@ -491,18 +487,12 @@ impl KomorebiNotificationState {
self.hide_empty_workspaces = config.hide_empty_workspaces;
}
#[allow(clippy::too_many_arguments)]
pub fn handle_notification(
&mut self,
ctx: &Context,
monitor_index: usize,
rx_gui: Receiver<komorebi_client::Notification>,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
transparency_alpha: Option<u8>,
grouping: Option<Grouping>,
default_theme: Option<KomobarTheme>,
render_config: Rc<RefCell<RenderConfig>>,
) {
match rx_gui.try_recv() {
Err(error) => match error {
@@ -520,42 +510,13 @@ impl KomorebiNotificationState {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
apply_theme(ctx, KomobarTheme::from(theme), bg_color.clone());
tracing::info!("applied theme from updated komorebi.json");
} else if let Some(default_theme) = default_theme {
apply_theme(
ctx,
default_theme,
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("removed theme from updated komorebi.json and applied default theme");
} else {
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
}
}
}
SocketMessage::Theme(theme) => {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color,
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
@@ -649,38 +610,17 @@ impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
impl From<&Container> for KomorebiNotificationStateContainerInformation {
fn from(value: &Container) -> Self {
let windows = value.windows().iter().collect::<Vec<_>>();
let mut icons = vec![];
for window in windows {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let exe = window.exe().unwrap_or_default();
match icon_cache.get(&exe) {
None => {
icons.push(windows_icons::get_icon_by_process_id(window.process_id()));
update_cache = true;
}
Some(icon) => {
icons.push(Some(icon.clone()));
}
}
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
}
}
}
Self {
titles: value
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
icons,
icons: value
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
focused_window_idx: value.focused_window_idx(),
}
}
@@ -688,30 +628,9 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
impl From<&Window> for KomorebiNotificationStateContainerInformation {
fn from(value: &Window) -> Self {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let mut icons = vec![];
let exe = value.exe().unwrap_or_default();
match icon_cache.get(&exe) {
None => {
icons.push(windows_icons::get_icon_by_process_id(value.process_id()));
update_cache = true;
}
Some(icon) => {
icons.push(Some(icon.clone()));
}
}
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
}
}
Self {
titles: vec![value.title().unwrap_or_default()],
icons,
icons: vec![windows_icons::get_icon_by_process_id(value.process_id())],
focused_window_idx: 0,
}
}

View File

@@ -10,6 +10,7 @@ use eframe::egui::Label;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use komorebi_client::SocketMessage;
@@ -224,7 +225,13 @@ impl KomorebiLayout {
workspace_idx: Option<usize>,
) {
let monitor_idx = render_config.monitor_idx;
let font_id = render_config.icon_font_id.clone();
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);

View File

@@ -13,7 +13,6 @@ mod selected_frame;
mod storage;
mod time;
mod ui;
mod update;
mod widget;
use crate::bar::Komobar;
@@ -25,11 +24,9 @@ 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;
@@ -37,8 +34,6 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
@@ -58,9 +53,6 @@ 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 ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
@@ -352,10 +344,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);

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)
@@ -42,13 +41,12 @@ impl From<NetworkConfig> for Network {
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),
show_total_activity: value.show_total_data_transmitted,
show_activity: value.show_network_activity,
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
@@ -65,7 +63,6 @@ pub struct Network {
pub enable: bool,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
@@ -100,7 +97,7 @@ impl Network {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh(true);
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
@@ -138,12 +135,7 @@ impl Network {
(activity, total_activity)
}
fn reading_to_label(
&self,
ctx: &Context,
reading: NetworkReading,
config: RenderConfig,
) -> Label {
fn reading_to_label(&self, ctx: &Context, reading: NetworkReading) -> Label {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
@@ -183,16 +175,16 @@ impl Network {
},
};
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()
};
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let icon_format =
TextFormat::simple(font_id.clone(), ctx.style().visuals.selection.stroke.color);
let text_format = TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color());
// icon
let mut layout_job = LayoutJob::simple(
@@ -258,77 +250,74 @@ impl Network {
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 || 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_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)
}
}
if self.show_total_activity {
for reading in total_activity {
config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading));
});
}
}
// widget spacing: pass on the config that was use for calling the apply_on_widget function
*config = render_config.clone();
if self.show_activity {
for reading in activity {
config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading));
});
}
}
}
if self.enable {
self.default_interface();
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
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)
}
}
});
}
}
}
}

View File

@@ -1,14 +1,11 @@
use crate::bar::Alignment;
use crate::config::KomobarConfig;
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;
@@ -16,7 +13,6 @@ 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);
@@ -33,7 +29,7 @@ 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,
@@ -49,38 +45,14 @@ 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);
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
RenderConfig {
monitor_idx: self.monitor.index,
spacing: self.widget_spacing.unwrap_or(10.0),
@@ -89,8 +61,6 @@ impl RenderExt for &KomobarConfig {
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id,
icon_font_id,
}
}
}
@@ -113,33 +83,21 @@ impl RenderConfig {
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>(
@@ -201,26 +159,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
@@ -230,60 +178,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 {
@@ -335,20 +239,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,4 +1,3 @@
use eframe::egui::CursorIcon;
use eframe::egui::Frame;
use eframe::egui::Margin;
use eframe::egui::Response;
@@ -51,6 +50,6 @@ impl SelectableFrame {
response
})
.inner
.on_hover_cursor(CursorIcon::PointingHand)
.on_hover_cursor(eframe::egui::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,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;
@@ -80,6 +81,13 @@ impl BarWidget for Time {
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 => {
@@ -87,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,
);
@@ -99,17 +107,16 @@ impl BarWidget for Time {
layout_job.append(
&output,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
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()
{
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

@@ -17,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;
@@ -40,7 +38,6 @@ pub enum WidgetConfig {
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
Update(UpdateConfig),
}
impl WidgetConfig {
@@ -55,30 +52,6 @@ impl WidgetConfig {
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::Komorebi(config) => {
config.workspaces.as_ref().map_or(false, |w| w.enable)
|| config.layout.as_ref().map_or(false, |w| w.enable)
|| config.focused_window.as_ref().map_or(false, |w| w.enable)
|| config
.configuration_switcher
.as_ref()
.map_or(false, |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.33"
version = "0.1.31"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -68,20 +68,6 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
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);

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-gui"
version = "0.1.33"
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,12 +1,12 @@
[package]
name = "komorebi-themes"
version = "0.1.33"
version = "0.1.31"
edition = "2021"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "24362c4" }
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 }

View File

@@ -1,7 +1,8 @@
[package]
name = "komorebi"
version = "0.1.33"
version = "0.1.31"
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
@@ -47,7 +48,7 @@ windows-core = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.53"
winreg = "0.52"
[build-dependencies]
shadow-rs = { workspace = true }

View File

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

View File

@@ -425,18 +425,6 @@ impl Border {
return LRESULT(0);
}
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);

View File

@@ -169,7 +169,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);
@@ -239,19 +238,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;
}
@@ -331,15 +321,22 @@ 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);
windows_borders.insert(
monocle.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
{
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 =
@@ -353,12 +350,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
border.invalidate();
borders_monitors.insert(monocle.id().clone(), monitor_idx);
windows_borders.insert(
monocle.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
for (id, b) in borders.iter() {
@@ -378,11 +369,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![];
@@ -427,7 +416,7 @@ 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() {
// 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()) {
@@ -445,14 +434,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
borders_monitors.insert(c.id().clone(), monitor_idx);
windows_borders.insert(
c.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
#[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 {
@@ -460,7 +452,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
{
@@ -471,17 +462,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
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(reference_hwnd)?;
let should_invalidate = match last_focus_state {
None => true,
@@ -495,16 +476,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
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(),
);
}
{
for window in ws.floating_windows() {
'windows: for window in ws.floating_windows() {
let mut new_border = false;
let border = match borders.entry(window.hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
@@ -520,15 +495,33 @@ 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);
windows_borders.insert(window.hwnd, border.clone());
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 =
@@ -549,9 +542,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if should_invalidate {
border.invalidate();
}
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
windows_borders.insert(window.hwnd, border.clone());
}
}
}

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

@@ -77,7 +77,6 @@ pub enum SocketMessage {
Promote,
PromoteFocus,
PromoteWindow(OperationDirection),
EagerFocus(String),
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
@@ -106,7 +105,6 @@ pub enum SocketMessage {
NewWorkspace,
ToggleTiling,
Stop,
StopIgnoreRestore,
TogglePause,
Retile,
RetileWithResizeDimensions,
@@ -186,7 +184,6 @@ pub enum SocketMessage {
ClearWorkspaceRules(usize, usize),
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
EnforceWorkspaceRules,
#[serde(alias = "FloatRule")]
IgnoreRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
@@ -238,9 +235,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,

View File

@@ -215,7 +215,7 @@ lazy_static! {
// 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()));
@@ -298,7 +298,6 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
| 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;
@@ -44,7 +43,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 +156,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]
@@ -177,7 +172,7 @@ fn main() -> Result<()> {
SESSION_ID.store(session_id, Ordering::SeqCst);
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All, true);
system.refresh_processes(ProcessesToUpdate::All);
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
@@ -265,13 +260,6 @@ fn main() -> Result<()> {
}
}
let dumped_state = temp_dir().join("komorebi.state.json");
if !opts.clean_state && dumped_state.is_file() {
let state: State = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?)?;
wm.lock().apply_state(state);
}
wm.lock().retile_all(false)?;
listen_for_events(wm.clone());
@@ -302,12 +290,9 @@ 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)?;
wm.lock().restore_all_windows()?;
AnimationEngine::wait_for_all_animations();
if WindowsApi::focus_follows_mouse()? {

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,6 +22,7 @@ use schemars::gen::SchemaSettings;
use schemars::schema_for;
use uds_windows::UnixStream;
use crate::animation::AnimationEngine;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
@@ -65,7 +67,6 @@ use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::WorkspaceWindowLocation;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
@@ -230,65 +231,6 @@ 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) => {
self.focus_container_in_direction(direction)?;
}
@@ -456,13 +398,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();
@@ -877,7 +812,9 @@ impl WindowManager {
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
@@ -897,7 +834,7 @@ impl WindowManager {
.remove(focused_workspace_idx)
.is_some()
{
self.focus_workspace(next_focused_workspace_idx)?;
self.focus_workspace(last_focused_workspace)?;
}
}
}
@@ -975,10 +912,30 @@ impl WindowManager {
}
}
SocketMessage::Stop => {
self.stop(false)?;
}
SocketMessage::StopIgnoreRestore => {
self.stop(true)?;
tracing::info!(
"received stop command, restoring all hidden windows and terminating process"
);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
self.restore_all_windows()?;
AnimationEngine::wait_for_all_animations();
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();
@@ -1293,7 +1250,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(
@@ -1662,7 +1619,6 @@ impl WindowManager {
}
SocketMessage::StackbarMode(mode) => {
STACKBAR_MODE.store(mode);
self.retile_all(true)?;
}
SocketMessage::StackbarLabel(label) => {
STACKBAR_LABEL.store(label);
@@ -1728,24 +1684,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 => {

View File

@@ -353,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())
{

View File

@@ -34,8 +34,6 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
let mut wm = arc.lock();
let offset = wm.work_area_offset;
let mut update_borders = false;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
@@ -53,7 +51,7 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
update_borders = true;
border_manager::send_notification(None);
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
@@ -64,9 +62,5 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
}
}
}
if update_borders {
border_manager::send_notification(None);
}
}
}

View File

@@ -35,7 +35,6 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::Axis;
use crate::CrossBoundaryBehaviour;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
@@ -47,7 +46,6 @@ use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
@@ -149,9 +147,6 @@ pub struct WorkspaceConfig {
/// (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 {
@@ -206,7 +201,6 @@ impl From<&Workspace> for WorkspaceConfig {
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
window_container_behaviour: *value.window_container_behaviour(),
float_override: *value.float_override(),
layout_flip: value.layout_flip(),
}
}
}
@@ -243,7 +237,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.33`
/// The `komorebi.json` static configuration file reference for `v0.1.31`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -379,9 +373,6 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
// this option is a little special because it is only consumed by komorebic
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>>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -636,7 +627,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()),
}
}
}
@@ -783,7 +773,6 @@ impl StaticConfig {
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)?;
@@ -829,10 +818,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);

View File

@@ -784,7 +784,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)]
@@ -889,19 +889,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)
};
{

View File

@@ -1,9 +1,7 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::env::temp_dir;
use std::io::ErrorKind;
use std::net::Shutdown;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
@@ -22,11 +20,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
use crate::animation::AnimationEngine;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::core::config_generation::MatchingRule;
use crate::core::custom_layout::CustomLayout;
use crate::core::Arrangement;
@@ -56,7 +50,6 @@ use crate::current_virtual_desktop;
use crate::load_configuration;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::should_act;
use crate::should_act_individual;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
@@ -67,8 +60,6 @@ use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::static_config::StaticConfig;
use crate::transparency_manager;
use crate::transparency_manager::TRANSPARENCY_ALPHA;
use crate::transparency_manager::TRANSPARENCY_ENABLED;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
@@ -91,8 +82,6 @@ use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::REMOVE_TITLEBARS;
use crate::SUBSCRIPTION_SOCKETS;
use crate::TRANSPARENCY_BLACKLIST;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_MATCHING_RULES;
@@ -197,9 +186,6 @@ pub struct GlobalState {
pub stackbar_tab_background_colour: Colour,
pub stackbar_tab_width: i32,
pub stackbar_height: i32,
pub transparency_enabled: bool,
pub transparency_alpha: u8,
pub transparency_blacklist: Vec<MatchingRule>,
pub remove_titlebars: bool,
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Vec<MatchingRule>,
@@ -253,9 +239,6 @@ impl Default for GlobalState {
)),
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
transparency_enabled: TRANSPARENCY_ENABLED.load(Ordering::SeqCst),
transparency_alpha: TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
transparency_blacklist: TRANSPARENCY_BLACKLIST.lock().clone(),
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
@@ -370,151 +353,6 @@ impl WindowManager {
WindowsApi::load_workspace_information(&mut self.monitors)
}
#[tracing::instrument(skip(self, state))]
pub fn apply_state(&mut self, state: State) {
let mut can_apply = true;
let state_monitors_len = state.monitors.elements().len();
let current_monitors_len = self.monitors.elements().len();
if state_monitors_len != current_monitors_len {
tracing::warn!(
"cannot apply state from {}; state file has {state_monitors_len} monitors, but only {current_monitors_len} are currently connected",
temp_dir().join("komorebi.state.json").to_string_lossy()
);
return;
}
for monitor in state.monitors.elements() {
for workspace in monitor.workspaces() {
for container in workspace.containers() {
for window in container.windows() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
}
if let Some(window) = workspace.maximized_window() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
}
for window in workspace.floating_windows() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
}
}
if can_apply {
tracing::info!(
"applying state from {}",
temp_dir().join("komorebi.state.json").to_string_lossy()
);
let offset = self.work_area_offset;
let mouse_follows_focus = self.mouse_follows_focus;
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
let mut focused_workspace = 0;
for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) {
if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
{
// to make sure padding changes get applied for users after a quick restart
let container_padding = workspace.container_padding();
let workspace_padding = workspace.workspace_padding();
*workspace = state_workspace.clone();
workspace.set_container_padding(container_padding);
workspace.set_workspace_padding(workspace_padding);
if state_monitor.focused_workspace_idx() == workspace_idx {
focused_workspace = workspace_idx;
}
}
}
}
if let Err(error) = monitor.focus_workspace(focused_workspace) {
tracing::warn!(
"cannot focus workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = monitor.load_focused_workspace(mouse_follows_focus) {
tracing::warn!(
"cannot load focused workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = monitor.update_focused_workspace(offset) {
tracing::warn!(
"cannot update workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
}
let focused_monitor_idx = state.monitors.focused_idx();
let focused_workspace_idx = state
.monitors
.elements()
.get(focused_monitor_idx)
.map(|m| m.focused_workspace_idx())
.unwrap_or_default();
if let Err(error) = self.focus_monitor(focused_monitor_idx) {
tracing::warn!(
"cannot focus monitor '{focused_monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = self.focus_workspace(focused_workspace_idx) {
tracing::warn!(
"cannot focus workspace '{focused_workspace_idx}' on monitor '{focused_monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = self.update_focused_workspace(true, true) {
tracing::warn!(
"cannot update focused workspace '{focused_workspace_idx}' on monitor '{focused_monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
} else {
tracing::warn!(
"cannot apply state from {}; some windows referenced in the state file no longer exist",
temp_dir().join("komorebi.state.json").to_string_lossy()
);
}
}
#[tracing::instrument]
pub fn reload_configuration() {
tracing::info!("reloading configuration");
@@ -731,80 +569,75 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.focused_workspace_idx();
// scope mutex locks to avoid deadlock if should_update_focused_workspace evaluates to true
// at the end of this function
{
let workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
// Go through all the monitors and workspaces
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
// And all the visible windows (at the top of a container)
for window in workspace.visible_windows().into_iter().flatten() {
let mut already_moved_window_handles =
self.already_moved_window_handles.lock();
let workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
// Go through all the monitors and workspaces
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
// And all the visible windows (at the top of a container)
for window in workspace.visible_windows().into_iter().flatten() {
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
let exe_name = window.exe()?;
let title = window.title()?;
let class = window.class()?;
let path = window.path()?;
if let (Ok(exe_name), Ok(title), Ok(class), Ok(path)) =
(window.exe(), window.title(), window.class(), window.path())
{
for rule in &*workspace_matching_rules {
let matched = match &rule.matching_rule {
MatchingRule::Simple(r) => should_act_individual(
for rule in &*workspace_matching_rules {
let matched = match &rule.matching_rule {
MatchingRule::Simple(r) => should_act_individual(
&title,
&exe_name,
&class,
&path,
r,
&regex_identifiers,
),
MatchingRule::Composite(r) => {
let mut composite_results = vec![];
for identifier in r {
composite_results.push(should_act_individual(
&title,
&exe_name,
&class,
&path,
r,
identifier,
&regex_identifiers,
),
MatchingRule::Composite(r) => {
let mut composite_results = vec![];
for identifier in r {
composite_results.push(should_act_individual(
&title,
&exe_name,
&class,
&path,
identifier,
&regex_identifiers,
));
}
composite_results.iter().all(|&x| x)
}
};
if matched {
let floating = workspace.floating_windows().contains(window);
if rule.initial_only {
if !already_moved_window_handles.contains(&window.hwnd) {
already_moved_window_handles.insert(window.hwnd);
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
} else {
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
));
}
composite_results.iter().all(|&x| x)
}
};
if matched {
let floating = workspace.floating_windows().contains(window);
if rule.initial_only {
if !already_moved_window_handles.contains(&window.hwnd) {
already_moved_window_handles.insert(window.hwnd);
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
} else {
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
}
}
@@ -1409,45 +1242,10 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn stop(&mut self, ignore_restore: bool) -> Result<()> {
tracing::info!(
"received stop command, restoring all hidden windows and terminating process"
);
let state = &State::from(&*self);
std::fs::write(
temp_dir().join("komorebi.state.json"),
serde_json::to_string_pretty(&state)?,
)?;
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
self.restore_all_windows(ignore_restore)?;
AnimationEngine::wait_for_all_animations();
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)
}
#[tracing::instrument(skip(self))]
pub fn restore_all_windows(&mut self, ignore_restore: bool) -> Result<()> {
pub fn restore_all_windows(&mut self) -> Result<()> {
tracing::info!("restoring all hidden windows");
let no_titlebar = NO_TITLEBAR.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let known_transparent_hwnds = transparency_manager::known_hwnds();
let border_implementation = border_manager::IMPLEMENTATION.load();
@@ -1463,17 +1261,7 @@ impl WindowManager {
for containers in workspace.containers_mut() {
for window in containers.windows_mut() {
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_titlebar_for_window {
if no_titlebar.contains(&window.exe()?) {
window.add_title_bar()?;
}
@@ -1485,9 +1273,7 @@ impl WindowManager {
window.remove_accent()?;
}
if !ignore_restore {
window.restore();
}
window.restore();
}
}
}
@@ -2261,7 +2047,7 @@ impl WindowManager {
let len = NonZeroUsize::new(container.windows().len())
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
if len.get() == 1 && idx != 0 {
if len.get() == 1 {
bail!("there is only one window in this container");
}
@@ -3286,10 +3072,6 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no container"))
}
pub fn focused_container_idx(&self) -> Result<usize> {
Ok(self.focused_workspace()?.focused_container_idx())
}
pub fn focused_container_mut(&mut self) -> Result<&mut Container> {
self.focused_workspace_mut()?
.focused_container_mut()

View File

@@ -24,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;
@@ -36,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)]
@@ -119,28 +117,16 @@ impl Default for Workspace {
}
}
#[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 {
@@ -191,10 +177,6 @@ impl Workspace {
self.set_float_override(config.float_override);
}
if config.layout_flip.is_some() {
self.set_layout_flip(config.layout_flip);
}
Ok(())
}
@@ -372,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();
@@ -402,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()?;
}
@@ -595,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,7 +1,8 @@
[package]
name = "komorebic-no-console"
version = "0.1.33"
version = "0.1.31"
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

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

View File

@@ -1,5 +1,3 @@
use shadow_rs::ShadowBuilder;
fn main() {
if std::fs::metadata("applications.json").is_err() {
let applications_json = reqwest::blocking::get(
@@ -8,5 +6,5 @@ fn main() {
std::fs::write("applications.json", applications_json).unwrap();
}
ShadowBuilder::builder().build().unwrap();
shadow_rs::new().unwrap();
}

View File

@@ -35,7 +35,6 @@ use miette::SourceSpan;
use paste::paste;
use schemars::gen::SchemaSettings;
use schemars::schema_for;
use serde::Deserialize;
use sysinfo::ProcessesToUpdate;
use which::which;
use windows::Win32::Foundation::HWND;
@@ -721,13 +720,6 @@ struct BorderImplementation {
style: komorebi_client::BorderImplementation,
}
#[derive(Parser)]
struct StackbarMode {
/// Desired stackbar mode
#[clap(value_enum)]
mode: komorebi_client::StackbarMode,
}
#[derive(Parser)]
struct Animation {
#[clap(value_enum)]
@@ -790,9 +782,6 @@ struct Start {
/// Start masir in a background process for focus-follows-mouse
#[clap(long)]
masir: bool,
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
#[clap(long)]
clean_state: bool,
}
#[derive(Parser)]
@@ -809,9 +798,6 @@ struct Stop {
/// Stop masir if it is running as a background process
#[clap(long)]
masir: bool,
/// Do not restore windows after stopping komorebi
#[clap(long, hide = true)]
ignore_restore: bool,
}
#[derive(Parser)]
@@ -929,25 +915,12 @@ struct EnableAutostart {
masir: bool,
}
#[derive(Parser)]
struct Check {
/// Path to a static configuration JSON file
#[clap(action, short, long)]
komorebi_config: Option<PathBuf>,
}
#[derive(Parser)]
struct ReplaceConfiguration {
/// Static configuration JSON file from which the configuration should be loaded
path: PathBuf,
}
#[derive(Parser)]
struct EagerFocus {
/// Case-sensitive exe identifier
exe: String,
}
#[derive(Parser)]
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
struct Opts {
@@ -968,7 +941,7 @@ enum SubCommand {
/// Kill background processes started by komorebic
Kill(Kill),
/// Check komorebi configuration and related files for common errors
Check(Check),
Check,
/// Show the path to komorebi.json
#[clap(alias = "config")]
Configuration,
@@ -1041,9 +1014,6 @@ enum SubCommand {
/// Move the focused window in the specified cycle direction
#[clap(arg_required_else_help = true)]
CycleMove(CycleMove),
/// Focus the first managed window matching the given exe
#[clap(arg_required_else_help = true)]
EagerFocus(EagerFocus),
/// Stack the focused window in the specified direction
#[clap(arg_required_else_help = true)]
Stack(Stack),
@@ -1333,8 +1303,6 @@ enum SubCommand {
ClearNamedWorkspaceRules(ClearNamedWorkspaceRules),
/// Remove all application association rules for all workspaces
ClearAllWorkspaceRules,
/// Enforce all workspace rules, including initial workspace rules that have already been applied
EnforceWorkspaceRules,
/// Identify an application that sends EVENT_OBJECT_NAMECHANGE on launch
#[clap(arg_required_else_help = true)]
IdentifyObjectNameChangeApplication(IdentifyObjectNameChangeApplication),
@@ -1375,9 +1343,6 @@ enum SubCommand {
/// Set the border implementation
#[clap(arg_required_else_help = true)]
BorderImplementation(BorderImplementation),
/// Set the stackbar mode
#[clap(arg_required_else_help = true)]
StackbarMode(StackbarMode),
/// Enable or disable transparency for unfocused windows
#[clap(arg_required_else_help = true)]
Transparency(Transparency),
@@ -1569,7 +1534,7 @@ fn main() -> Result<()> {
arguments.push_str(" --ahk");
}
if args.masir {
if args.bar {
arguments.push_str(" --masir");
}
@@ -1593,7 +1558,7 @@ fn main() -> Result<()> {
std::fs::remove_file(shortcut_file)?;
}
}
SubCommand::Check(args) => {
SubCommand::Check => {
let home_display = HOME_DIR.display();
if HAS_CUSTOM_CONFIG_HOME.load(Ordering::SeqCst) {
println!("KOMOREBI_CONFIG_HOME detected: {home_display}\n");
@@ -1608,15 +1573,7 @@ fn main() -> Result<()> {
println!("Looking for configuration files in {home_display}\n");
let static_config = if let Some(static_config) = args.komorebi_config {
println!(
"Using an arbitrary configuration file passed to --komorebi-config flag\n"
);
static_config
} else {
HOME_DIR.join("komorebi.json")
};
let static_config = HOME_DIR.join("komorebi.json");
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");
let config_whkd = WHKD_CONFIG_DIR.join("whkdrc");
@@ -1693,30 +1650,6 @@ fn main() -> Result<()> {
println!("No komorebi configuration found in {home_display}\n");
println!("If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration\n");
}
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebic-version-checker")
.send()
{
let version = env!("CARGO_PKG_VERSION");
#[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");
if trimmed > version {
println!("An updated version of komorebi is available! https://github.com/LGUG2Z/komorebi/releases/v{trimmed}");
}
}
}
}
SubCommand::Configuration => {
let static_config = HOME_DIR.join("komorebi.json");
@@ -1785,9 +1718,6 @@ fn main() -> Result<()> {
SubCommand::CycleMove(arg) => {
send_message(&SocketMessage::CycleMoveWindow(arg.cycle_direction))?;
}
SubCommand::EagerFocus(arg) => {
send_message(&SocketMessage::EagerFocus(arg.exe))?;
}
SubCommand::MoveToMonitor(arg) => {
send_message(&SocketMessage::MoveContainerToMonitorNumber(arg.target))?;
}
@@ -2082,10 +2012,6 @@ fn main() -> Result<()> {
flags.push(format!("'--tcp-port={port}'"));
}
if arg.clean_state {
flags.push("'--clean-state'".to_string());
}
let script = if flags.is_empty() {
format!(
"Start-Process '{}' -WindowStyle hidden",
@@ -2100,7 +2026,7 @@ fn main() -> Result<()> {
};
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All, true);
system.refresh_processes(ProcessesToUpdate::All);
let mut attempts = 0;
let mut running = system
@@ -2121,7 +2047,7 @@ fn main() -> Result<()> {
print!("Waiting for komorebi.exe to start...");
std::thread::sleep(Duration::from_secs(3));
system.refresh_processes(ProcessesToUpdate::All, true);
system.refresh_processes(ProcessesToUpdate::All);
if system
.processes_by_name("komorebi.exe".as_ref())
@@ -2257,16 +2183,14 @@ if (!(Get-Process masir -ErrorAction SilentlyContinue))
}
println!("\nThank you for using komorebi!\n");
println!("# Commercial Use License");
println!("* View licensing options https://lgug2z.com/software/komorebi - A commercial use license is required to use komorebi at work");
println!("\n# Personal Use Sponsorship");
println!("# Sponsorship");
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - $5/month makes a big difference");
println!("* Leave a tip https://ko-fi.com/lgug2z - An alternative to GitHub Sponsors");
println!("\n# Community");
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
println!(
"* Subscribe to https://youtube.com/@LGUG2Z - Development videos, feature previews and release overviews"
);
println!("\n# Community");
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
println!("* Explore the Awesome Komorebi list https://github.com/LGUG2Z/awesome-komorebi - Projects in the komorebi ecosystem");
println!("\n# Documentation");
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
@@ -2296,30 +2220,6 @@ if (!(Get-Process masir -ErrorAction SilentlyContinue))
let stdout = String::from_utf8(output.stdout)?;
println!("{stdout}");
}
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebic-version-checker")
.send()
{
let version = env!("CARGO_PKG_VERSION");
#[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");
if trimmed > version {
println!("An updated version of komorebi is available! https://github.com/LGUG2Z/komorebi/releases/v{trimmed}");
}
}
}
}
SubCommand::Stop(arg) => {
if arg.whkd {
@@ -2393,13 +2293,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
}
}
if arg.ignore_restore {
send_message(&SocketMessage::StopIgnoreRestore)?;
} else {
send_message(&SocketMessage::Stop)?;
}
send_message(&SocketMessage::Stop)?;
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All, true);
system.refresh_processes(ProcessesToUpdate::All);
if system.processes_by_name("komorebi.exe".as_ref()).count() >= 1 {
println!("komorebi is still running, attempting to force-quit");
@@ -2547,9 +2443,6 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::ClearAllWorkspaceRules => {
send_message(&SocketMessage::ClearAllWorkspaceRules)?;
}
SubCommand::EnforceWorkspaceRules => {
send_message(&SocketMessage::EnforceWorkspaceRules)?;
}
SubCommand::Stack(arg) => {
send_message(&SocketMessage::StackWindow(arg.operation_direction))?;
}
@@ -2795,9 +2688,6 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::BorderImplementation(arg) => {
send_message(&SocketMessage::BorderImplementation(arg.style))?;
}
SubCommand::StackbarMode(arg) => {
send_message(&SocketMessage::StackbarMode(arg.mode))?;
}
SubCommand::Transparency(arg) => {
send_message(&SocketMessage::Transparency(arg.boolean_state.into()))?;
}

View File

@@ -77,7 +77,6 @@ nav:
- cli/quickstart.md
- cli/start.md
- cli/stop.md
- cli/kill.md
- cli/check.md
- cli/configuration.md
- cli/bar-configuration.md
@@ -104,11 +103,9 @@ nav:
- cli/force-focus.md
- cli/cycle-focus.md
- cli/cycle-move.md
- cli/eager-focus.md
- cli/stack.md
- cli/unstack.md
- cli/cycle-stack.md
- cli/cycle-stack-index.md
- cli/focus-stack-window.md
- cli/stack-all.md
- cli/unstack-all.md
@@ -132,7 +129,6 @@ nav:
- cli/focus-workspaces.md
- cli/focus-monitor-workspace.md
- cli/focus-named-workspace.md
- cli/close-workspace.md
- cli/cycle-monitor.md
- cli/cycle-workspace.md
- cli/move-workspace-to-monitor.md
@@ -200,7 +196,6 @@ nav:
- cli/clear-workspace-rules.md
- cli/clear-named-workspace-rules.md
- cli/clear-all-workspace-rules.md
- cli/enforce-workspace-rules.md
- cli/identify-object-name-change-application.md
- cli/identify-tray-application.md
- cli/identify-layered-application.md
@@ -212,7 +207,6 @@ nav:
- cli/border-offset.md
- cli/border-style.md
- cli/border-implementation.md
- cli/stackbar-mode.md
- cli/transparency.md
- cli/transparency-alpha.md
- cli/toggle-transparency.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.33`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.31`",
"type": "object",
"properties": {
"animation": {
@@ -13,35 +13,13 @@
"properties": {
"duration": {
"description": "Set the animation duration in ms (default: 250)",
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
]
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"enabled": {
"description": "Enable or disable animations (default: false)",
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "boolean"
}
},
{
"type": "boolean"
}
]
"type": "boolean"
},
"fps": {
"description": "Set the animation FPS (default: 60)",
@@ -51,80 +29,38 @@
},
"style": {
"description": "Set the animation style (default: Linear)",
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
]
}
},
{
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
]
}
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
]
}
}
@@ -484,7 +420,7 @@
"format": "int32"
},
"border_z_order": {
"description": "DEPRECATED from v0.1.31: no longer required",
"description": "Active window border z-order (default: System)",
"type": "string",
"enum": [
"Top",
@@ -643,7 +579,7 @@
}
},
"focus_follows_mouse": {
"description": "END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead",
"description": "END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)",
"oneOf": [
{
"description": "A custom FFM implementation (slightly more CPU-intensive)",
@@ -1227,15 +1163,6 @@
"RightMainVerticalStack"
]
},
"layout_flip": {
"description": "Specify an axis on which to flip the selected layout (default: None)",
"type": "string",
"enum": [
"Horizontal",
"Vertical",
"HorizontalAndVertical"
]
},
"layout_rules": {
"description": "Layout rules (default: None)",
"type": "object",
@@ -1457,89 +1384,6 @@
]
}
},
"remove_titlebar_applications": {
"description": "HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars",
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"type": "string",
"enum": [
"Exe",
"Class",
"Title",
"Path"
]
},
"matching_strategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}
},
{
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"type": "string",
"enum": [
"Exe",
"Class",
"Title",
"Path"
]
},
"matching_strategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}
}
}
]
}
},
"resize_delta": {
"description": "Delta to resize windows by (default 50)",
"type": "integer",

View File

@@ -7,6 +7,5 @@ with pkgs;
python311Packages.mkdocs-material
python311Packages.mkdocs-macros
python311Packages.setuptools
python311Packages.json-schema-for-humans
];
}