mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-10 07:17:41 +01:00
Compare commits
66 Commits
feature/ta
...
update-dep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c12afe5521 | ||
|
|
2d97ee101d | ||
|
|
a4f69238b7 | ||
|
|
96f7eb1d31 | ||
|
|
28cd4a8801 | ||
|
|
3aa92a1255 | ||
|
|
281980b010 | ||
|
|
c063302c91 | ||
|
|
ba52dc3378 | ||
|
|
44716fdc98 | ||
|
|
4b30cecba9 | ||
|
|
d45cd729e8 | ||
|
|
5a8f48c6b9 | ||
|
|
4b9d811499 | ||
|
|
d520a2bf74 | ||
|
|
7ef4fd81c0 | ||
|
|
083ab65077 | ||
|
|
e9bb6b43d6 | ||
|
|
79eda30f48 | ||
|
|
692da90890 | ||
|
|
4babf336ec | ||
|
|
53a83eedb5 | ||
|
|
e1bbd3c1f5 | ||
|
|
2c08fbe8f6 | ||
|
|
cced2a4433 | ||
|
|
d93b6fa1b3 | ||
|
|
99353b8064 | ||
|
|
c64a42bca5 | ||
|
|
5ab5ec4f3a | ||
|
|
ad08585faf | ||
|
|
eb8a988841 | ||
|
|
0e2a55b300 | ||
|
|
eda91dcd1d | ||
|
|
0c6317a27b | ||
|
|
5c81a8c9e2 | ||
|
|
a4128b7276 | ||
|
|
73a4df884c | ||
|
|
32a234317c | ||
|
|
0dc6780da6 | ||
|
|
f089d3e59b | ||
|
|
5dbf0f1b89 | ||
|
|
d393f8fe77 | ||
|
|
c3769e7881 | ||
|
|
3c0b12f9af | ||
|
|
804faef229 | ||
|
|
7bf1521363 | ||
|
|
b49e634b65 | ||
|
|
be0671be6d | ||
|
|
10539a4bab | ||
|
|
9463c75f12 | ||
|
|
c31c5dc69d | ||
|
|
6c07863b81 | ||
|
|
40c55dec39 | ||
|
|
5cc2d9d469 | ||
|
|
91b255280a | ||
|
|
9bd1073a83 | ||
|
|
53c1990442 | ||
|
|
9d6173ecbb | ||
|
|
830da89529 | ||
|
|
f59d7a51f1 | ||
|
|
1470c63cfe | ||
|
|
64382b18c1 | ||
|
|
26f90cc9ee | ||
|
|
192af6751b | ||
|
|
4f306e5bfd | ||
|
|
ede0b23bb4 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
labels: [ bug ]
|
||||
labels: [bug]
|
||||
title: "[BUG]: "
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
4
.github/workflows/windows.yaml
vendored
4
.github/workflows/windows.yaml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
key: ${{ matrix.platform.target }}
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
- uses: houseabsolute/actions-rust-cross@v0
|
||||
- uses: houseabsolute/actions-rust-cross@v1
|
||||
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@v2
|
||||
- uses: vedantmgoyal2009/winget-releaser@main
|
||||
with:
|
||||
identifier: LGUG2Z.komorebi
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
717
Cargo.lock
generated
717
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -17,8 +17,8 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.29"
|
||||
egui_extras = "0.29"
|
||||
eframe = "0.30"
|
||||
egui_extras = "0.30"
|
||||
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.31"
|
||||
sysinfo = "0.33"
|
||||
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.35"
|
||||
shadow-rs = "0.37"
|
||||
which = "7"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
|
||||
115
README.md
115
README.md
@@ -29,6 +29,8 @@ Tiling Window Management for Windows.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
@@ -50,6 +52,8 @@ _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
|
||||
@@ -57,28 +61,62 @@ repository.
|
||||
|
||||
There is a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
_komorebi_ development videos. If you would like to be notified of upcoming
|
||||
videos please subscribe and turn on notifications.
|
||||
_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.
|
||||
|
||||
_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
|
||||
## 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 contributing
|
||||
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). 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.
|
||||
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).
|
||||
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).
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -125,7 +163,11 @@ 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.
|
||||
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.
|
||||
|
||||
## Commit hygiene
|
||||
|
||||
@@ -135,8 +177,8 @@ If you would like to contribute to `komorebi` please take the time to carefully
|
||||
- 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
|
||||
|
||||
@@ -175,7 +217,8 @@ 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
|
||||
@@ -191,27 +234,6 @@ 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
|
||||
@@ -220,13 +242,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.
|
||||
@@ -367,7 +389,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.30"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.32"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
@@ -442,12 +464,17 @@ 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`
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
```
|
||||
Set the duration for movement animations in ms
|
||||
|
||||
Usage: komorebic.exe animation-duration <DURATION>
|
||||
Usage: komorebic.exe animation-duration [OPTIONS] <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
|
||||
|
||||
|
||||
@@ -10,8 +10,14 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo,
|
||||
ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart,
|
||||
ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back,
|
||||
ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the style to. If not specified, sets global style
|
||||
|
||||
[possible values: movement, transparency]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
```
|
||||
Enable or disable movement animations
|
||||
|
||||
Usage: komorebic.exe animation <BOOLEAN_STATE>
|
||||
Usage: komorebic.exe animation [OPTIONS] <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
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
```
|
||||
Check komorebi configuration and related files for common errors
|
||||
|
||||
Usage: komorebic.exe check
|
||||
Usage: komorebic.exe check [OPTIONS]
|
||||
|
||||
Options:
|
||||
-k, --komorebi-config <KOMOREBI_CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
12
docs/cli/close-workspace.md
Normal file
12
docs/cli/close-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# close-workspace
|
||||
|
||||
```
|
||||
Close the focused workspace (must be empty and unnamed)
|
||||
|
||||
Usage: komorebic.exe close-workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/cycle-stack-index.md
Normal file
16
docs/cli/cycle-stack-index.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
|
||||
```
|
||||
16
docs/cli/eager-focus.md
Normal file
16
docs/cli/eager-focus.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
|
||||
```
|
||||
@@ -18,6 +18,9 @@ Options:
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
--masir
|
||||
Enable autostart of masir
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
12
docs/cli/enforce-workspace-rules.md
Normal file
12
docs/cli/enforce-workspace-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 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
|
||||
|
||||
```
|
||||
24
docs/cli/kill.md
Normal file
24
docs/cli/kill.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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
|
||||
|
||||
```
|
||||
18
docs/cli/stackbar-mode.md
Normal file
18
docs/cli/stackbar-mode.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
|
||||
```
|
||||
@@ -24,6 +24,12 @@ 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
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ 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
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# toggle-workspace-float-override
|
||||
|
||||
```
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes the opposite of the global value
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes
|
||||
the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-float-override
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```
|
||||
{% 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"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||

|
||||
|
||||
## 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
|
||||
@@ -15,12 +17,63 @@ 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. If you have any
|
||||
specific feature requests or bugs to report, please create an issue on
|
||||
[GitHub](https://github.com/LGUG2Z/komorebi).
|
||||
`komorebi`-related discussion, help, troubleshooting etc.
|
||||
|
||||
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 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.
|
||||
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).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.32/schema.bar.json",
|
||||
"monitor": {
|
||||
"index": 0,
|
||||
"work_area_offset": {
|
||||
@@ -33,6 +33,11 @@
|
||||
}
|
||||
],
|
||||
"right_widgets": [
|
||||
{
|
||||
"Update": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Media": {
|
||||
"enable": true
|
||||
@@ -73,4 +78,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.32/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
15
justfile
15
justfile
@@ -1,4 +1,5 @@
|
||||
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
|
||||
|
||||
export RUST_BACKTRACE := "full"
|
||||
|
||||
clean:
|
||||
@@ -45,13 +46,15 @@ docgen:
|
||||
cargo run --package komorebic -- docgen
|
||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
||||
|
||||
schemagen:
|
||||
jsonschema:
|
||||
cargo run --package komorebic -- static-config-schema > schema.json
|
||||
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
|
||||
cargo run --package komorebi-bar -- --schema > schema.bar.json
|
||||
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
|
||||
|
||||
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
|
||||
|
||||
rm -Force .\bar-config-docs\schema.html
|
||||
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -16,15 +16,16 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.7"
|
||||
egui-phosphor = "0.8"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
netdev = "0.31"
|
||||
netdev = "0.32"
|
||||
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 }
|
||||
|
||||
@@ -38,6 +38,7 @@ 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;
|
||||
@@ -49,6 +50,7 @@ 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>>>,
|
||||
@@ -58,10 +60,21 @@ 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>>) {
|
||||
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>>,
|
||||
) {
|
||||
match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
name: catppuccin,
|
||||
@@ -141,6 +154,29 @@ pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Colo
|
||||
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 {
|
||||
@@ -160,7 +196,136 @@ impl Komobar {
|
||||
Self::add_custom_font(ctx, font_family);
|
||||
}
|
||||
|
||||
let position = config.position.clone().unwrap_or(PositionConfig {
|
||||
// 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 {
|
||||
start: Some(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
@@ -171,42 +336,40 @@ impl Komobar {
|
||||
}),
|
||||
});
|
||||
|
||||
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 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")
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
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());
|
||||
apply_theme(
|
||||
ctx,
|
||||
theme,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
config.transparency_alpha,
|
||||
config.grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|
||||
@@ -222,11 +385,21 @@ 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());
|
||||
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(),
|
||||
);
|
||||
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
@@ -247,124 +420,28 @@ 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(
|
||||
@@ -374,6 +451,7 @@ 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,
|
||||
@@ -383,7 +461,10 @@ 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);
|
||||
@@ -427,7 +508,7 @@ impl Komobar {
|
||||
if let Some((font, _)) = system_fonts::get(&property) {
|
||||
fonts
|
||||
.font_data
|
||||
.insert(name.to_owned(), FontData::from_owned(font));
|
||||
.insert(name.to_owned(), Arc::new(FontData::from_owned(font)));
|
||||
|
||||
fonts
|
||||
.families
|
||||
@@ -453,6 +534,10 @@ 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(
|
||||
@@ -478,71 +563,135 @@ 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.borrow())
|
||||
.fill(*self.bg_color_with_alpha.borrow())
|
||||
} else {
|
||||
Frame::none().fill(*self.bg_color.borrow())
|
||||
Frame::none().fill(*self.bg_color_with_alpha.borrow())
|
||||
};
|
||||
|
||||
let mut render_config = self.render_config.borrow_mut();
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |_| {
|
||||
// Apply grouping logic for the bar as a whole
|
||||
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);
|
||||
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.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.left_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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.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);
|
||||
|
||||
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);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.center_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ 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;
|
||||
@@ -30,37 +29,18 @@ pub struct BatteryConfig {
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
fn from(value: BatteryConfig) -> Self {
|
||||
let manager = Manager::new().unwrap();
|
||||
let mut last_state = String::new();
|
||||
let mut state = None;
|
||||
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
|
||||
|
||||
if let Ok(mut batteries) = manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
match first.state() {
|
||||
State::Charging => state = Some(BatteryState::Charging),
|
||||
State::Discharging => state = Some(BatteryState::Discharging),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
last_state = match prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
manager,
|
||||
last_state,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: prefix,
|
||||
state: state.unwrap_or(BatteryState::Discharging),
|
||||
last_updated: Instant::now(),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,19 +104,12 @@ 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(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -144,7 +117,12 @@ impl BarWidget for Battery {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
|
||||
@@ -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.31`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.33`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar positioning options
|
||||
#[serde(alias = "viewport")]
|
||||
@@ -25,6 +25,8 @@ 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
|
||||
@@ -188,12 +190,16 @@ pub enum LabelPrefix {
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum DisplayFormat {
|
||||
/// Show only icon
|
||||
Icon,
|
||||
/// Show only text
|
||||
Text,
|
||||
/// Show an icon and text for the selected element, and text on the rest
|
||||
TextAndIconOnSelected,
|
||||
/// Show both icon and text
|
||||
IconAndText,
|
||||
/// Show an icon and text for the selected element, and icons on the rest
|
||||
IconAndTextOnSelected,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
@@ -30,17 +29,18 @@ pub struct CpuConfig {
|
||||
|
||||
impl From<CpuConfig> for Cpu {
|
||||
fn from(value: CpuConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
|
||||
|
||||
system.refresh_cpu_usage();
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_memory().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,13 +74,6 @@ 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 => {
|
||||
@@ -88,7 +81,7 @@ impl BarWidget for Cpu {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -96,16 +89,17 @@ impl BarWidget for Cpu {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
@@ -90,13 +89,6 @@ 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 => {
|
||||
@@ -104,7 +96,7 @@ impl BarWidget for Date {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -116,16 +108,22 @@ impl BarWidget for Date {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
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),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
|
||||
@@ -2,10 +2,12 @@ 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;
|
||||
@@ -14,7 +16,6 @@ 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;
|
||||
@@ -22,7 +23,6 @@ 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: KomorebiWorkspacesConfig,
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
/// Configure the Layout widget
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
/// Configure the Focused Window widget
|
||||
@@ -113,7 +113,10 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
selected_workspace: String::new(),
|
||||
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
workspaces: vec![],
|
||||
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
|
||||
hide_empty_workspaces: value
|
||||
.workspaces
|
||||
.map(|w| w.hide_empty_workspaces)
|
||||
.unwrap_or_default(),
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
|
||||
@@ -131,7 +134,7 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Komorebi {
|
||||
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
@@ -140,140 +143,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 self.workspaces.enable {
|
||||
let mut update = None;
|
||||
if let Some(workspaces) = self.workspaces {
|
||||
if workspaces.enable {
|
||||
let mut update = None;
|
||||
|
||||
if !komorebi_notification_state.workspaces.is_empty() {
|
||||
let format = self.workspaces.display.unwrap_or(DisplayFormat::Text);
|
||||
if !komorebi_notification_state.workspaces.is_empty() {
|
||||
let format = 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;
|
||||
|
||||
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)
|
||||
.shrink_to_fit(),
|
||||
);
|
||||
|
||||
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 font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
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()
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, container_information)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
if SelectableFrame::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
)
|
||||
.show(ui, |ui| {
|
||||
let mut has_icon = false;
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
.is_err()
|
||||
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 !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 !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);
|
||||
|
||||
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()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
update = Some(ws.to_string());
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(
|
||||
&SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
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(
|
||||
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");
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,9 +371,10 @@ 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;
|
||||
let selected = i == focused_window_idx && len != 1;
|
||||
|
||||
if SelectableFrame::new(selected)
|
||||
.show(ui, |ui| {
|
||||
@@ -383,7 +387,11 @@ impl BarWidget for Komorebi {
|
||||
},
|
||||
);
|
||||
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format
|
||||
if format == DisplayFormat::Icon
|
||||
|| format == DisplayFormat::IconAndText
|
||||
|| format == DisplayFormat::IconAndTextOnSelected
|
||||
|| (format == DisplayFormat::TextAndIconOnSelected
|
||||
&& i == focused_window_idx)
|
||||
{
|
||||
if let Some(img) = icon {
|
||||
Frame::none()
|
||||
@@ -394,7 +402,7 @@ impl BarWidget for Komorebi {
|
||||
let response = ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.shrink_to_fit(),
|
||||
.fit_to_exact_size(icon_size),
|
||||
);
|
||||
|
||||
if let DisplayFormat::Icon = format {
|
||||
@@ -404,7 +412,11 @@ impl BarWidget for Komorebi {
|
||||
}
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format
|
||||
if format == DisplayFormat::Text
|
||||
|| format == DisplayFormat::IconAndText
|
||||
|| format == DisplayFormat::TextAndIconOnSelected
|
||||
|| (format == DisplayFormat::IconAndTextOnSelected
|
||||
&& i == focused_window_idx)
|
||||
{
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
@@ -424,35 +436,27 @@ impl BarWidget for Komorebi {
|
||||
return;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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() {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -487,12 +491,18 @@ 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 {
|
||||
@@ -510,13 +520,42 @@ 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());
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("removed theme from updated komorebi.json and applied default theme");
|
||||
} else {
|
||||
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
@@ -610,17 +649,38 @@ 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: value
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
icons,
|
||||
focused_window_idx: value.focused_window_idx(),
|
||||
}
|
||||
}
|
||||
@@ -628,9 +688,30 @@ 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: vec![windows_icons::get_icon_by_process_id(value.process_id())],
|
||||
icons,
|
||||
focused_window_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ 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;
|
||||
@@ -225,13 +224,7 @@ impl KomorebiLayout {
|
||||
workspace_idx: Option<usize>,
|
||||
) {
|
||||
let monitor_idx = render_config.monitor_idx;
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let font_id = render_config.icon_font_id.clone();
|
||||
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
|
||||
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ mod selected_frame;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
mod update;
|
||||
mod widget;
|
||||
|
||||
use crate::bar::Komobar;
|
||||
@@ -24,9 +25,11 @@ 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;
|
||||
@@ -34,6 +37,8 @@ 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;
|
||||
@@ -53,6 +58,9 @@ 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 {
|
||||
@@ -344,6 +352,10 @@ 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);
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
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;
|
||||
@@ -82,16 +81,9 @@ 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(),
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -99,24 +91,28 @@ impl BarWidget for Media {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
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);
|
||||
|
||||
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(),
|
||||
)
|
||||
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(),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
@@ -30,17 +29,18 @@ pub struct MemoryConfig {
|
||||
|
||||
impl From<MemoryConfig> for Memory {
|
||||
fn from(value: MemoryConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
|
||||
|
||||
system.refresh_memory();
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_cpu().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,13 +77,6 @@ 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 => {
|
||||
@@ -91,7 +84,7 @@ impl BarWidget for Memory {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -99,16 +92,17 @@ impl BarWidget for Memory {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
@@ -27,6 +26,8 @@ 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)
|
||||
@@ -41,12 +42,13 @@ 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(),
|
||||
@@ -63,6 +65,7 @@ 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,
|
||||
@@ -97,7 +100,7 @@ impl Network {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh();
|
||||
self.networks_network_activity.refresh(true);
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
@@ -135,7 +138,12 @@ impl Network {
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn reading_to_label(&self, ctx: &Context, reading: NetworkReading) -> Label {
|
||||
fn reading_to_label(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
reading: NetworkReading,
|
||||
config: RenderConfig,
|
||||
) -> Label {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
@@ -175,16 +183,16 @@ impl Network {
|
||||
},
|
||||
};
|
||||
|
||||
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());
|
||||
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()
|
||||
};
|
||||
|
||||
// icon
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
@@ -250,74 +258,77 @@ impl Network {
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 {
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// widget spacing: make sure to use the same config to call the apply_on_widget function
|
||||
let mut render_config = config.clone();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
if self.show_total_activity || self.show_activity {
|
||||
let (activity, total_activity) = self.network_activity();
|
||||
|
||||
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: ");
|
||||
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()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// widget spacing: pass on the config that was use for calling the apply_on_widget function
|
||||
*config = render_config.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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;
|
||||
@@ -13,6 +16,7 @@ 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);
|
||||
|
||||
@@ -29,7 +33,7 @@ pub enum Grouping {
|
||||
Widget(GroupingConfig),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct RenderConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub monitor_idx: usize,
|
||||
@@ -45,14 +49,38 @@ 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, background_color: Color32) -> RenderConfig;
|
||||
fn new_renderconfig(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
background_color: Color32,
|
||||
icon_scale: Option<f32>,
|
||||
) -> RenderConfig;
|
||||
}
|
||||
|
||||
impl RenderExt for &KomobarConfig {
|
||||
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
|
||||
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);
|
||||
|
||||
RenderConfig {
|
||||
monitor_idx: self.monitor.index,
|
||||
spacing: self.widget_spacing.unwrap_or(10.0),
|
||||
@@ -61,6 +89,8 @@ impl RenderExt for &KomobarConfig {
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id,
|
||||
icon_font_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,21 +113,33 @@ impl RenderConfig {
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id: FontId::default(),
|
||||
icon_font_id: FontId::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_on_bar<R>(
|
||||
pub fn change_frame_on_bar(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
frame: Frame,
|
||||
ui_style: &Arc<eframe::egui::Style>,
|
||||
) -> Frame {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Bar(config) = self.grouping {
|
||||
return self.define_group(None, config, ui, add_contents);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
Self::fallback_group(ui, add_contents)
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn apply_on_alignment<R>(
|
||||
@@ -159,16 +201,26 @@ impl RenderConfig {
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
Frame::group(ui.style_mut())
|
||||
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)
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(8.0, 3.0),
|
||||
false => Margin::symmetric(3.0, 3.0),
|
||||
true => Margin::symmetric(6.0, 1.0),
|
||||
false => Margin::symmetric(1.0, 1.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
|
||||
@@ -178,16 +230,60 @@ impl RenderConfig {
|
||||
Some(style) => match style {
|
||||
// new styles can be added if needed here
|
||||
GroupingStyle::Default => Shadow::NONE,
|
||||
GroupingStyle::DefaultWithShadow => Shadow {
|
||||
GroupingStyle::DefaultWithShadowB4O1S3 => 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 {
|
||||
@@ -239,9 +335,20 @@ pub struct GroupingConfig {
|
||||
pub enum GroupingStyle {
|
||||
#[serde(alias = "CtByte")]
|
||||
Default,
|
||||
/// A black shadow is added under the default group
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
||||
#[serde(alias = "CtByteWithShadow")]
|
||||
DefaultWithShadow,
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use eframe::egui::CursorIcon;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Response;
|
||||
@@ -50,6 +51,6 @@ impl SelectableFrame {
|
||||
response
|
||||
})
|
||||
.inner
|
||||
.on_hover_cursor(eframe::egui::CursorIcon::PointingHand)
|
||||
.on_hover_cursor(CursorIcon::PointingHand)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
@@ -51,7 +50,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();
|
||||
self.disks.refresh(true);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
@@ -81,13 +80,6 @@ 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 {
|
||||
@@ -96,7 +88,7 @@ impl BarWidget for Storage {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -104,16 +96,17 @@ impl BarWidget for Storage {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
@@ -81,13 +80,6 @@ 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 => {
|
||||
@@ -95,7 +87,7 @@ impl BarWidget for Time {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -107,16 +99,17 @@ impl BarWidget for Time {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
|
||||
158
komorebi-bar/src/update.rs
Normal file
158
komorebi-bar/src/update.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ 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;
|
||||
@@ -38,6 +40,7 @@ pub enum WidgetConfig {
|
||||
Network(NetworkConfig),
|
||||
Storage(StorageConfig),
|
||||
Time(TimeConfig),
|
||||
Update(UpdateConfig),
|
||||
}
|
||||
|
||||
impl WidgetConfig {
|
||||
@@ -52,6 +55,30 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -68,6 +68,20 @@ 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);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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"] }
|
||||
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"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
|
||||
@@ -48,7 +47,7 @@ windows-core = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.52"
|
||||
winreg = "0.53"
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use shadow_rs::ShadowBuilder;
|
||||
|
||||
fn main() {
|
||||
shadow_rs::new().unwrap();
|
||||
ShadowBuilder::builder().build().unwrap();
|
||||
}
|
||||
|
||||
@@ -425,6 +425,18 @@ 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);
|
||||
|
||||
@@ -169,6 +169,7 @@ 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);
|
||||
|
||||
@@ -238,10 +239,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// when we switch focus to a floating window
|
||||
if !should_process_notification
|
||||
&& floating_window_hwnds.contains(¬ification.0.unwrap_or_default())
|
||||
{
|
||||
// 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 == ¬ification.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 {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
@@ -321,22 +331,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
windows_borders.insert(
|
||||
monocle.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
border.clone(),
|
||||
);
|
||||
|
||||
let new_focus_state = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
};
|
||||
border.window_kind = new_focus_state;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let reference_hwnd =
|
||||
@@ -350,6 +353,12 @@ 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() {
|
||||
@@ -369,9 +378,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
);
|
||||
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);
|
||||
|
||||
if is_maximized {
|
||||
let mut to_remove = vec![];
|
||||
@@ -416,7 +427,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
'containers: 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()) {
|
||||
@@ -434,17 +445,14 @@ 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 {
|
||||
@@ -452,6 +460,7 @@ 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
|
||||
{
|
||||
@@ -462,7 +471,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let reference_hwnd =
|
||||
c.focused_window().copied().unwrap_or_default().hwnd;
|
||||
|
||||
let rect = WindowsApi::window_rect(reference_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 should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
@@ -476,10 +495,16 @@ 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(),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
'windows: for window in ws.floating_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(),
|
||||
@@ -495,33 +520,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
windows_borders.insert(window.hwnd, border.clone());
|
||||
|
||||
let mut should_destroy = false;
|
||||
|
||||
if let Some(notification_hwnd) = notification.0 {
|
||||
if notification_hwnd != window.hwnd {
|
||||
should_destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if WindowsApi::foreground_window().unwrap_or_default()
|
||||
!= window.hwnd
|
||||
{
|
||||
should_destroy = true;
|
||||
}
|
||||
|
||||
if should_destroy {
|
||||
border.destroy()?;
|
||||
borders.remove(&window.hwnd.to_string());
|
||||
borders_monitors.remove(&window.hwnd.to_string());
|
||||
continue 'windows;
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
let new_focus_state = WindowKind::Floating;
|
||||
let mut new_focus_state = WindowKind::Unfocused;
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
new_focus_state = WindowKind::Floating;
|
||||
}
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state =
|
||||
@@ -542,6 +549,9 @@ 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,18 @@ 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 {
|
||||
|
||||
@@ -77,6 +77,7 @@ pub enum SocketMessage {
|
||||
Promote,
|
||||
PromoteFocus,
|
||||
PromoteWindow(OperationDirection),
|
||||
EagerFocus(String),
|
||||
ToggleFloat,
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
@@ -105,6 +106,7 @@ pub enum SocketMessage {
|
||||
NewWorkspace,
|
||||
ToggleTiling,
|
||||
Stop,
|
||||
StopIgnoreRestore,
|
||||
TogglePause,
|
||||
Retile,
|
||||
RetileWithResizeDimensions,
|
||||
@@ -184,6 +186,7 @@ pub enum SocketMessage {
|
||||
ClearWorkspaceRules(usize, usize),
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
EnforceWorkspaceRules,
|
||||
#[serde(alias = "FloatRule")]
|
||||
IgnoreRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
@@ -235,7 +238,9 @@ pub struct SubscribeOptions {
|
||||
pub filter_state_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(
|
||||
Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema, ValueEnum,
|
||||
)]
|
||||
pub enum StackbarMode {
|
||||
Always,
|
||||
Never,
|
||||
|
||||
@@ -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<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
@@ -298,6 +298,7 @@ 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(¬ification)?;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
clippy::doc_markdown
|
||||
)]
|
||||
|
||||
use std::env::temp_dir;
|
||||
use std::net::Shutdown;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -43,6 +44,7 @@ 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;
|
||||
@@ -156,6 +158,9 @@ 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]
|
||||
@@ -172,7 +177,7 @@ fn main() -> Result<()> {
|
||||
SESSION_ID.store(session_id, Ordering::SeqCst);
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
|
||||
|
||||
@@ -260,6 +265,13 @@ 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());
|
||||
@@ -290,9 +302,12 @@ 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()?;
|
||||
wm.lock().restore_all_windows(false)?;
|
||||
AnimationEngine::wait_for_all_animations();
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -22,7 +21,6 @@ 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;
|
||||
@@ -67,6 +65,7 @@ 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;
|
||||
@@ -231,6 +230,65 @@ 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)?;
|
||||
}
|
||||
@@ -398,6 +456,13 @@ 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();
|
||||
|
||||
@@ -812,9 +877,7 @@ impl WindowManager {
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
let last_focused_workspace = monitor
|
||||
.last_focused_workspace()
|
||||
.unwrap_or(focused_workspace_idx.saturating_sub(1));
|
||||
let next_focused_workspace_idx = focused_workspace_idx.saturating_sub(1);
|
||||
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if monitor.workspaces().len() > 1
|
||||
@@ -834,7 +897,7 @@ impl WindowManager {
|
||||
.remove(focused_workspace_idx)
|
||||
.is_some()
|
||||
{
|
||||
self.focus_workspace(last_focused_workspace)?;
|
||||
self.focus_workspace(next_focused_workspace_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -912,30 +975,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
SocketMessage::Stop => {
|
||||
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)
|
||||
self.stop(false)?;
|
||||
}
|
||||
SocketMessage::StopIgnoreRestore => {
|
||||
self.stop(true)?;
|
||||
}
|
||||
SocketMessage::MonitorIndexPreference(index_preference, left, top, right, bottom) => {
|
||||
let mut monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
|
||||
@@ -1250,7 +1293,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()?;
|
||||
self.restore_all_windows(false)?;
|
||||
|
||||
// Create a new wm from the config path
|
||||
let mut wm = StaticConfig::preload(
|
||||
@@ -1619,6 +1662,7 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::StackbarMode(mode) => {
|
||||
STACKBAR_MODE.store(mode);
|
||||
self.retile_all(true)?;
|
||||
}
|
||||
SocketMessage::StackbarLabel(label) => {
|
||||
STACKBAR_LABEL.store(label);
|
||||
@@ -1684,10 +1728,24 @@ impl WindowManager {
|
||||
|
||||
reply.write_all(config.as_bytes())?;
|
||||
}
|
||||
SocketMessage::RemoveTitleBar(_, ref id) => {
|
||||
SocketMessage::RemoveTitleBar(identifier, ref id) => {
|
||||
let mut identifiers = NO_TITLEBAR.lock();
|
||||
if !identifiers.contains(id) {
|
||||
identifiers.push(id.clone());
|
||||
|
||||
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),
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleTitleBars => {
|
||||
|
||||
@@ -353,10 +353,11 @@ 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())
|
||||
{
|
||||
|
||||
@@ -34,6 +34,8 @@ 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 = (
|
||||
@@ -51,7 +53,7 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
let reaped_orphans = workspace.reap_orphans()?;
|
||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||
workspace.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
border_manager::send_notification(None);
|
||||
update_borders = true;
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
@@ -62,5 +64,9 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update_borders {
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ 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;
|
||||
@@ -46,6 +47,7 @@ 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;
|
||||
@@ -147,6 +149,9 @@ 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 {
|
||||
@@ -201,6 +206,7 @@ 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,7 +243,7 @@ impl From<&Monitor> for MonitorConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.31`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.33`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -373,6 +379,9 @@ 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)]
|
||||
@@ -627,6 +636,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
),
|
||||
slow_application_identifiers: Option::from(SLOW_APPLICATION_IDENTIFIERS.lock().clone()),
|
||||
bar_configurations: None,
|
||||
remove_titlebar_applications: Option::from(NO_TITLEBAR.lock().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -773,6 +783,7 @@ 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)?;
|
||||
@@ -818,6 +829,10 @@ 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);
|
||||
|
||||
@@ -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<String>,
|
||||
pub matches_no_titlebar: Option<MatchingRule>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -889,9 +889,19 @@ fn window_is_eligible(
|
||||
allow
|
||||
};
|
||||
|
||||
let allow_titlebar_removed = {
|
||||
let titlebars_removed = NO_TITLEBAR.lock();
|
||||
titlebars_removed.contains(exe_name)
|
||||
let titlebars_removed = NO_TITLEBAR.lock();
|
||||
let allow_titlebar_removed = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&titlebars_removed,
|
||||
®ex_identifiers,
|
||||
) {
|
||||
debug.matches_no_titlebar = Some(rule);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
@@ -20,7 +22,11 @@ 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;
|
||||
@@ -50,6 +56,7 @@ 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;
|
||||
@@ -60,6 +67,8 @@ 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;
|
||||
@@ -82,6 +91,8 @@ 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;
|
||||
|
||||
@@ -186,6 +197,9 @@ 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>,
|
||||
@@ -239,6 +253,9 @@ 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(),
|
||||
@@ -353,6 +370,151 @@ 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");
|
||||
@@ -569,75 +731,80 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
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()?;
|
||||
// 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();
|
||||
|
||||
for rule in &*workspace_matching_rules {
|
||||
let matched = match &rule.matching_rule {
|
||||
MatchingRule::Simple(r) => should_act_individual(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
r,
|
||||
®ex_identifiers,
|
||||
),
|
||||
MatchingRule::Composite(r) => {
|
||||
let mut composite_results = vec![];
|
||||
for identifier in r {
|
||||
composite_results.push(should_act_individual(
|
||||
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(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
identifier,
|
||||
r,
|
||||
®ex_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,
|
||||
®ex_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,10 +1409,45 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn restore_all_windows(&mut self) -> Result<()> {
|
||||
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<()> {
|
||||
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();
|
||||
|
||||
@@ -1261,7 +1463,17 @@ impl WindowManager {
|
||||
|
||||
for containers in workspace.containers_mut() {
|
||||
for window in containers.windows_mut() {
|
||||
if no_titlebar.contains(&window.exe()?) {
|
||||
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,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_remove_titlebar_for_window {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
@@ -1273,7 +1485,9 @@ impl WindowManager {
|
||||
window.remove_accent()?;
|
||||
}
|
||||
|
||||
window.restore();
|
||||
if !ignore_restore {
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2047,7 +2261,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 {
|
||||
if len.get() == 1 && idx != 0 {
|
||||
bail!("there is only one window in this container");
|
||||
}
|
||||
|
||||
@@ -3072,6 +3286,10 @@ 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()
|
||||
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -35,6 +36,7 @@ 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)]
|
||||
@@ -117,16 +119,28 @@ 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 {
|
||||
@@ -177,6 +191,10 @@ impl Workspace {
|
||||
self.set_float_override(config.float_override);
|
||||
}
|
||||
|
||||
if config.layout_flip.is_some() {
|
||||
self.set_layout_flip(config.layout_flip);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -354,6 +372,7 @@ 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();
|
||||
@@ -383,9 +402,19 @@ impl Workspace {
|
||||
.focused_window()
|
||||
.is_some_and(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
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,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_remove_titlebars && should_remove_titlebar_for_window {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
} else if should_remove_titlebar_for_window {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
@@ -566,6 +595,41 @@ 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) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
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"
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
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"
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use shadow_rs::ShadowBuilder;
|
||||
|
||||
fn main() {
|
||||
if std::fs::metadata("applications.json").is_err() {
|
||||
let applications_json = reqwest::blocking::get(
|
||||
@@ -6,5 +8,5 @@ fn main() {
|
||||
std::fs::write("applications.json", applications_json).unwrap();
|
||||
}
|
||||
|
||||
shadow_rs::new().unwrap();
|
||||
ShadowBuilder::builder().build().unwrap();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ 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;
|
||||
@@ -720,6 +721,13 @@ 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)]
|
||||
@@ -782,6 +790,9 @@ 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)]
|
||||
@@ -798,6 +809,9 @@ 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)]
|
||||
@@ -915,12 +929,25 @@ 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 {
|
||||
@@ -941,7 +968,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,
|
||||
@@ -1014,6 +1041,9 @@ 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),
|
||||
@@ -1303,6 +1333,8 @@ 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),
|
||||
@@ -1343,6 +1375,9 @@ 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),
|
||||
@@ -1534,7 +1569,7 @@ fn main() -> Result<()> {
|
||||
arguments.push_str(" --ahk");
|
||||
}
|
||||
|
||||
if args.bar {
|
||||
if args.masir {
|
||||
arguments.push_str(" --masir");
|
||||
}
|
||||
|
||||
@@ -1558,7 +1593,7 @@ fn main() -> Result<()> {
|
||||
std::fs::remove_file(shortcut_file)?;
|
||||
}
|
||||
}
|
||||
SubCommand::Check => {
|
||||
SubCommand::Check(args) => {
|
||||
let home_display = HOME_DIR.display();
|
||||
if HAS_CUSTOM_CONFIG_HOME.load(Ordering::SeqCst) {
|
||||
println!("KOMOREBI_CONFIG_HOME detected: {home_display}\n");
|
||||
@@ -1573,7 +1608,15 @@ fn main() -> Result<()> {
|
||||
|
||||
println!("Looking for configuration files in {home_display}\n");
|
||||
|
||||
let static_config = HOME_DIR.join("komorebi.json");
|
||||
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 config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
let config_whkd = WHKD_CONFIG_DIR.join("whkdrc");
|
||||
@@ -1650,6 +1693,30 @@ 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");
|
||||
@@ -1718,6 +1785,9 @@ 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))?;
|
||||
}
|
||||
@@ -2012,6 +2082,10 @@ 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",
|
||||
@@ -2026,7 +2100,7 @@ fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
|
||||
let mut attempts = 0;
|
||||
let mut running = system
|
||||
@@ -2047,7 +2121,7 @@ fn main() -> Result<()> {
|
||||
print!("Waiting for komorebi.exe to start...");
|
||||
std::thread::sleep(Duration::from_secs(3));
|
||||
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
|
||||
if system
|
||||
.processes_by_name("komorebi.exe".as_ref())
|
||||
@@ -2183,14 +2257,16 @@ if (!(Get-Process masir -ErrorAction SilentlyContinue))
|
||||
}
|
||||
|
||||
println!("\nThank you for using komorebi!\n");
|
||||
println!("# Sponsorship");
|
||||
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!("* 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");
|
||||
@@ -2220,6 +2296,30 @@ 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 {
|
||||
@@ -2293,9 +2393,13 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
}
|
||||
}
|
||||
|
||||
send_message(&SocketMessage::Stop)?;
|
||||
if arg.ignore_restore {
|
||||
send_message(&SocketMessage::StopIgnoreRestore)?;
|
||||
} else {
|
||||
send_message(&SocketMessage::Stop)?;
|
||||
}
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
|
||||
if system.processes_by_name("komorebi.exe".as_ref()).count() >= 1 {
|
||||
println!("komorebi is still running, attempting to force-quit");
|
||||
@@ -2443,6 +2547,9 @@ 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))?;
|
||||
}
|
||||
@@ -2688,6 +2795,9 @@ 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()))?;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ nav:
|
||||
- cli/quickstart.md
|
||||
- cli/start.md
|
||||
- cli/stop.md
|
||||
- cli/kill.md
|
||||
- cli/check.md
|
||||
- cli/configuration.md
|
||||
- cli/bar-configuration.md
|
||||
@@ -103,9 +104,11 @@ 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
|
||||
@@ -129,6 +132,7 @@ 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
|
||||
@@ -196,6 +200,7 @@ 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
|
||||
@@ -207,6 +212,7 @@ 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
|
||||
|
||||
1407
schema.bar.json
1407
schema.bar.json
File diff suppressed because it is too large
Load Diff
234
schema.json
234
schema.json
@@ -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.31`",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.33`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
@@ -13,13 +13,35 @@
|
||||
"properties": {
|
||||
"duration": {
|
||||
"description": "Set the animation duration in ms (default: 250)",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Enable or disable animations (default: false)",
|
||||
"type": "boolean"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fps": {
|
||||
"description": "Set the animation FPS (default: 60)",
|
||||
@@ -29,38 +51,80 @@
|
||||
},
|
||||
"style": {
|
||||
"description": "Set the animation style (default: Linear)",
|
||||
"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"
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -420,7 +484,7 @@
|
||||
"format": "int32"
|
||||
},
|
||||
"border_z_order": {
|
||||
"description": "Active window border z-order (default: System)",
|
||||
"description": "DEPRECATED from v0.1.31: no longer required",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Top",
|
||||
@@ -579,7 +643,7 @@
|
||||
}
|
||||
},
|
||||
"focus_follows_mouse": {
|
||||
"description": "END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)",
|
||||
"description": "END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A custom FFM implementation (slightly more CPU-intensive)",
|
||||
@@ -1163,6 +1227,15 @@
|
||||
"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",
|
||||
@@ -1384,6 +1457,89 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user