mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-15 09:47:40 +01:00
Compare commits
83 Commits
v0.1.33
...
feature/wo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
557ef98ee5 | ||
|
|
3d327c407c | ||
|
|
e5fb5390a8 | ||
|
|
6a8e362c21 | ||
|
|
1edeb44203 | ||
|
|
8bc04f0610 | ||
|
|
30c22f51c9 | ||
|
|
c095f8ae9f | ||
|
|
c455ad1386 | ||
|
|
ce99290027 | ||
|
|
60bc83d407 | ||
|
|
9c8a639282 | ||
|
|
b7ebd3fe63 | ||
|
|
ec8519d75a | ||
|
|
c62405bfaa | ||
|
|
0126465de4 | ||
|
|
1cd28652aa | ||
|
|
a1ab1c5724 | ||
|
|
be932078e0 | ||
|
|
302e96c172 | ||
|
|
c05eab9044 | ||
|
|
ff986fba67 | ||
|
|
e408410c58 | ||
|
|
3ade81444a | ||
|
|
c9e98c3cdb | ||
|
|
b42fcbe509 | ||
|
|
d8636d651d | ||
|
|
9ad32e40cf | ||
|
|
c91cb9f061 | ||
|
|
52340a1487 | ||
|
|
4f7a8f10c0 | ||
|
|
c903cdbb75 | ||
|
|
80edcadbf7 | ||
|
|
36dedbe3fe | ||
|
|
5f31e89e8d | ||
|
|
d168013375 | ||
|
|
db6e12b0c2 | ||
|
|
e629baec0a | ||
|
|
475519d603 | ||
|
|
2d2b6e5c15 | ||
|
|
9f19d449b2 | ||
|
|
86bbcac5ae | ||
|
|
bbd232f649 | ||
|
|
2ca9c9048b | ||
|
|
95d758e371 | ||
|
|
83114ed3e7 | ||
|
|
f3075efcae | ||
|
|
80b611890a | ||
|
|
58d660eb16 | ||
|
|
afdbce3db1 | ||
|
|
be8af2b314 | ||
|
|
241f8a1375 | ||
|
|
bd0913a5f5 | ||
|
|
7f3b932693 | ||
|
|
59cd36a2b1 | ||
|
|
b8e8ac2cc9 | ||
|
|
c364b90b3b | ||
|
|
e2f7fe50c9 | ||
|
|
81c143d7c2 | ||
|
|
fcd1c9dcbe | ||
|
|
f73f0a0012 | ||
|
|
4a8362336f | ||
|
|
5c3c3659b5 | ||
|
|
4123c9a0e2 | ||
|
|
cfd89c274c | ||
|
|
4f041123d1 | ||
|
|
473e7cd6a0 | ||
|
|
0a2dbed116 | ||
|
|
067a279c58 | ||
|
|
1101baa722 | ||
|
|
da156c091e | ||
|
|
39621c14db | ||
|
|
e153d2ea0c | ||
|
|
e01c3e3c71 | ||
|
|
d7fcbb7d00 | ||
|
|
3e1fc6123a | ||
|
|
d09d16d291 | ||
|
|
77ef259ea8 | ||
|
|
392e4cc0c9 | ||
|
|
129dc5d43f | ||
|
|
eb6e12e2bd | ||
|
|
a069db611f | ||
|
|
b451df0379 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -8,9 +8,9 @@ body:
|
||||
value: |
|
||||
Please **do not** open an issue for applications with invisible windows leaving ghost tiles.
|
||||
|
||||
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
|
||||
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that information to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
|
||||
|
||||
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers , then this is not a bug with komorebi but a bug with the application you are using, and should open an issue with the developer(s) of that application.
|
||||
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers, then this is not a bug with komorebi but a bug with the application you are using, and you should open an issue with the developer(s) of that application.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
16
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,21 +1,21 @@
|
||||
name: Feature request
|
||||
description: Suggest a new feature (Sponsors only)
|
||||
description: Suggest a new feature (Limited to Sponsors, Commercial License Holders, and Collaborators)
|
||||
labels: [enhancement]
|
||||
title: "[FEAT]: "
|
||||
body:
|
||||
- type: dropdown
|
||||
id: Sponsors
|
||||
id: Eligibility
|
||||
attributes:
|
||||
label: Sponsorship Information
|
||||
label: Eligibility
|
||||
description: >
|
||||
Feature requests are considered from individuals who are $5+ monthly sponsors to the project.
|
||||
Feature requests are considered from individuals who are current $5+ monthly sponsors to the project, individual commercial use license holders, and approved collaborators.
|
||||
|
||||
Please specify the platform you use to sponsor the project.
|
||||
options:
|
||||
- GitHub Sponsors
|
||||
- Ko-fi
|
||||
- Discord
|
||||
- YouTube
|
||||
- Individual Commercial Use License
|
||||
- GitHub Sponsor
|
||||
- Ko-fi Sponsor
|
||||
- Approved Collaborator
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
47
.github/workflows/feature-check.yaml
vendored
Normal file
47
.github/workflows/feature-check.yaml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Feature Issue Check
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
|
||||
jobs:
|
||||
auto-close:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Check and close feature issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
if (issue.title.startsWith('[FEAT]: ')) {
|
||||
const message = `
|
||||
Feature requests on this repository are only open to current [GitHub sponsors](https://github.com/sponsors/LGUG2Z) on the $5/month tier and above, people with a valid [individual commercial use license](https://lgug2z.com/software/komorebi), and approved contributors.
|
||||
|
||||
This issue has been automatically closed until one of those pre-requisites can be validated.
|
||||
`.replace(/^\s+/gm, '');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: message,
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'resolved'
|
||||
});
|
||||
}
|
||||
125
.github/workflows/sponsor-check.yaml
vendored
125
.github/workflows/sponsor-check.yaml
vendored
@@ -1,125 +0,0 @@
|
||||
name: Feature Request Sponsor Check
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_username:
|
||||
description: "Test username to check sponsorship for"
|
||||
required: true
|
||||
default: "octocat"
|
||||
test_title:
|
||||
description: "Test issue title"
|
||||
required: true
|
||||
default: "[FEAT] Test Feature Request"
|
||||
test_sponsor_platform:
|
||||
description: "Selected sponsor platform"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- "GitHub Sponsors"
|
||||
- "Ko-fi"
|
||||
- "Discord"
|
||||
- "YouTube"
|
||||
|
||||
jobs:
|
||||
check-sponsor:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch') || (github.event_name == 'issues' &&
|
||||
startsWith(github.event.issue.title, '[FEAT]') &&
|
||||
github.event.issue.user.login != 'LGUG2Z' &&
|
||||
fromJSON(github.event.issue.body).Sponsors == 'GitHub Sponsors')
|
||||
|
||||
steps:
|
||||
- name: Get Issue Details
|
||||
id: issue-details
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "username=${{ github.event.inputs.test_username }}" >> $GITHUB_OUTPUT
|
||||
echo "title=${{ github.event.inputs.test_title }}" >> $GITHUB_OUTPUT
|
||||
echo "sponsor_platform=${{ github.event.inputs.test_sponsor_platform }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "username=${{ github.event.issue.user.login }}" >> $GITHUB_OUTPUT
|
||||
echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
|
||||
echo "sponsor_platform=$(jq -r '.Sponsors' <<< '${{ github.event.issue.body }}')" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get Sponsorship Status
|
||||
id: sponsorship
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.PAT }}
|
||||
script: |
|
||||
const username = '${{ steps.issue-details.outputs.username }}';
|
||||
const sponsorPlatform = '${{ steps.issue-details.outputs.sponsor_platform }}';
|
||||
|
||||
if (sponsorPlatform !== 'GitHub Sponsors') {
|
||||
console.log('Sponsor platform is not GitHub Sponsors, skipping check');
|
||||
return true;
|
||||
}
|
||||
|
||||
const sponsorshipQuery = `query($user: String!) {
|
||||
user(login: $user) {
|
||||
... on Sponsorable {
|
||||
sponsorshipForViewerAsSponsorable {
|
||||
tier {
|
||||
name
|
||||
monthlyPriceInDollars
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
try {
|
||||
const result = await github.graphql(sponsorshipQuery, {
|
||||
user: username
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
const sponsorship = result.user.sponsorshipForViewerAsSponsorable;
|
||||
console.log(sponsorship);
|
||||
const amount = sponsorship?.tier?.monthlyPriceInDollars || 0;
|
||||
|
||||
console.log(`Sponsorship amount for ${username}: $${amount}/month`);
|
||||
return amount >= 5;
|
||||
} catch (error) {
|
||||
console.log(`Error checking sponsorship: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
- name: Print Test Results
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Test Results for ${{ steps.issue-details.outputs.username }}:"
|
||||
echo "Title: ${{ steps.issue-details.outputs.title }}"
|
||||
echo "Platform: ${{ steps.issue-details.outputs.sponsor_platform }}"
|
||||
echo "Would close issue: ${{ steps.sponsorship.outputs.result == 'false' }}"
|
||||
|
||||
- name: Close Issue If Not Sponsor
|
||||
if: |
|
||||
github.event_name == 'issues' &&
|
||||
steps.sponsorship.outputs.result == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: 'Thank you for your feature request! This repository requires a GitHub sponsorship of at least $5/month to submit feature requests. Please consider becoming a sponsor at https://github.com/sponsors/LGUG2Z'
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
1
.github/workflows/windows.yaml
vendored
1
.github/workflows/windows.yaml
vendored
@@ -47,6 +47,7 @@ jobs:
|
||||
key: ${{ matrix.platform.target }}
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
- run: cargo test --package komorebi --test compat
|
||||
- uses: houseabsolute/actions-rust-cross@v1
|
||||
with:
|
||||
command: "build"
|
||||
|
||||
960
Cargo.lock
generated
960
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@@ -19,7 +19,7 @@ crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.30"
|
||||
egui_extras = "0.30"
|
||||
dirs = "5"
|
||||
dirs = "6"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
schemars = "0.8"
|
||||
@@ -33,27 +33,31 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
paste = "1"
|
||||
sysinfo = "0.33"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
|
||||
windows-implement = { version = "0.58" }
|
||||
windows-interface = { version = "0.58" }
|
||||
windows-core = { version = "0.58" }
|
||||
shadow-rs = "0.37"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "376523b9e1321e033b0b0ed0e6fa75a072b46ad9" }
|
||||
windows-numerics = { version = "0.1" }
|
||||
windows-implement = { version = "0.59" }
|
||||
windows-interface = { version = "0.59" }
|
||||
windows-core = { version = "0.60" }
|
||||
shadow-rs = "0.38"
|
||||
which = "7"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
version = "0.60"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
|
||||
@@ -389,7 +389,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.33"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.34"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# flip-layout
|
||||
|
||||
```
|
||||
Flip the layout on the focused workspace (BSP only)
|
||||
Flip the layout on the focused workspace
|
||||
|
||||
Usage: komorebic.exe flip-layout <AXIS>
|
||||
|
||||
|
||||
12
docs/cli/focus-monitor-at-cursor.md
Normal file
12
docs/cli/focus-monitor-at-cursor.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# focus-monitor-at-cursor
|
||||
|
||||
```
|
||||
Focus the monitor at the current cursor location
|
||||
|
||||
Usage: komorebic.exe focus-monitor-at-cursor
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
|
||||
|
||||
Arguments:
|
||||
<STATE_QUERY>
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index]
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
12
docs/cli/toggle-window-based-work-area-offset.md
Normal file
12
docs/cli/toggle-window-based-work-area-offset.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-window-based-work-area-offset
|
||||
|
||||
```
|
||||
Toggle application of the window-based work area offset for the focused workspace
|
||||
|
||||
Usage: komorebic.exe toggle-window-based-work-area-offset
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/toggle-workspace-layer.md
Normal file
12
docs/cli/toggle-workspace-layer.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-workspace-layer
|
||||
|
||||
```
|
||||
Toggle between the Tiling and Floating layers on the focused workspace
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-layer
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -26,5 +26,15 @@ If you already have configuration files that you wish to keep, move them to the
|
||||
The next time you run `komorebic start`, any files created by or loaded by
|
||||
_komorebi_ will be placed or expected to exist in this folder.
|
||||
|
||||
After setting `$Env:KOMOREBI_CONFIG_HOME`, make sure to update the path in komorebi.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"app_specific_configuration_path": "$Env:KOMOREBI_CONFIG_HOME/applications.json"
|
||||
}
|
||||
```
|
||||
|
||||
This ensures that komorebi can locate all configuration files correctly.
|
||||
|
||||
[](https://www.youtube.com/watch?v=C_KWUqQ6kko)
|
||||
|
||||
@@ -75,7 +75,7 @@ solo developer.
|
||||
|
||||
If you choose to use the active window border, you can set different colours to
|
||||
give you visual queues when you are focused on a single window, a stack of
|
||||
windows, or a window that is in monocole mode.
|
||||
windows, or a window that is in monocle mode.
|
||||
|
||||
The example colours given are blue single, green for stack and pink for
|
||||
monocle.
|
||||
@@ -254,5 +254,5 @@ stackbars as well as the status bar.
|
||||
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
|
||||
|
||||
All [Catppuccin palette variants](https://catppuccin.com/)
|
||||
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
|
||||
and [most Base16 palette variants](https://tinted-theming.github.io/tinted-gallery/)
|
||||
are available as themes.
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.33/schema.bar.json",
|
||||
"monitor": {
|
||||
"index": 0,
|
||||
"work_area_offset": {
|
||||
"left": 0,
|
||||
"top": 40,
|
||||
"right": 0,
|
||||
"bottom": 40
|
||||
}
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.33/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
9
justfile
9
justfile
@@ -24,6 +24,15 @@ install-target target:
|
||||
install:
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
build-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
|
||||
|
||||
build-target target:
|
||||
cargo +stable build --release --package {{ target }} --locked
|
||||
|
||||
build:
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -34,4 +34,5 @@ sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::config::get_individual_spacing;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
@@ -11,12 +13,15 @@ use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::KomorebiEvent;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Align2;
|
||||
use eframe::egui::Area;
|
||||
@@ -34,16 +39,20 @@ use eframe::egui::Margin;
|
||||
use eframe::egui::Rgba;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::Visuals;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::MonitorNotification;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -51,18 +60,21 @@ use std::sync::Arc;
|
||||
|
||||
pub struct Komobar {
|
||||
pub hwnd: Option<isize>,
|
||||
pub config: Arc<KomobarConfig>,
|
||||
pub monitor_index: Option<usize>,
|
||||
pub disabled: bool,
|
||||
pub config: KomobarConfig,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub rx_gui: Receiver<komorebi_client::Notification>,
|
||||
pub rx_gui: Receiver<KomorebiEvent>,
|
||||
pub rx_config: Receiver<KomobarConfig>,
|
||||
pub bg_color: Rc<RefCell<Color32>>,
|
||||
pub bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
pub scale_factor: f32,
|
||||
pub size_rect: komorebi_client::Rect,
|
||||
pub work_area_offset: komorebi_client::Rect,
|
||||
applied_theme_on_first_frame: bool,
|
||||
}
|
||||
|
||||
@@ -183,46 +195,45 @@ impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
config: &KomobarConfig,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
config.max_label_width.unwrap_or(400.0) as i32,
|
||||
self.config.max_label_width.unwrap_or(400.0) as i32,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
if let Some(font_family) = &config.font_family {
|
||||
if let Some(font_family) = &self.config.font_family {
|
||||
tracing::info!("attempting to add custom font family: {font_family}");
|
||||
Self::add_custom_font(ctx, font_family);
|
||||
}
|
||||
|
||||
// Update the `size_rect` so that the bar position can be changed on the EGUI update
|
||||
// function
|
||||
self.update_size_rect(config.position.clone());
|
||||
self.update_size_rect();
|
||||
|
||||
self.try_apply_theme(config, ctx);
|
||||
self.try_apply_theme(ctx);
|
||||
|
||||
if let Some(font_size) = &config.font_size {
|
||||
if let Some(font_size) = &self.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(
|
||||
self.render_config.replace((&self.config).new_renderconfig(
|
||||
ctx,
|
||||
*self.bg_color.borrow(),
|
||||
config.icon_scale,
|
||||
self.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() {
|
||||
for (idx, widget_config) in self.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 {
|
||||
if let Some(center_widgets) = &self.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));
|
||||
@@ -230,20 +241,21 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
|
||||
for (idx, widget_config) in self.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
|
||||
let mut left_widgets = self
|
||||
.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 {
|
||||
let mut center_widgets = match &self.config.center_widgets {
|
||||
Some(center_widgets) => center_widgets
|
||||
.iter()
|
||||
.filter(|config| config.enabled())
|
||||
@@ -252,7 +264,8 @@ impl Komobar {
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let mut right_widgets = config
|
||||
let mut right_widgets = self
|
||||
.config
|
||||
.right_widgets
|
||||
.iter()
|
||||
.filter(|config| config.enabled())
|
||||
@@ -294,38 +307,87 @@ impl Komobar {
|
||||
self.center_widgets = center_widgets;
|
||||
self.right_widgets = right_widgets;
|
||||
|
||||
if let (Some(prev_rect), Some(new_rect)) = (
|
||||
&self.config.monitor.work_area_offset,
|
||||
&config.monitor.work_area_offset,
|
||||
) {
|
||||
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
|
||||
);
|
||||
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
||||
(monitor_config.index, monitor_config.work_area_offset)
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
};
|
||||
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
|
||||
state
|
||||
.borrow()
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.copied()
|
||||
});
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
||||
{
|
||||
if new_rect != prev_rect {
|
||||
self.work_area_offset = *new_rect;
|
||||
if let Err(error) = komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(monitor_index, *new_rect),
|
||||
) {
|
||||
tracing::error!(
|
||||
"error applying work area offset to monitor '{}': {}",
|
||||
monitor_index,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
tracing::info!("work area offset applied to monitor: {}", monitor_index);
|
||||
}
|
||||
}
|
||||
} else if let Some(height) = self.config.height.or(Some(BAR_HEIGHT)) {
|
||||
// We only add the `bottom_margin` to the work_area_offset since the top margin is
|
||||
// already considered on the `size_rect.top`
|
||||
let bottom_margin = self
|
||||
.config
|
||||
.margin
|
||||
.as_ref()
|
||||
.map_or(0, |v| v.to_individual(0.0).bottom as i32);
|
||||
let new_rect = komorebi_client::Rect {
|
||||
left: 0,
|
||||
top: (height as i32)
|
||||
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
|
||||
+ bottom_margin,
|
||||
right: 0,
|
||||
bottom: (height as i32)
|
||||
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
|
||||
+ bottom_margin,
|
||||
};
|
||||
|
||||
if new_rect != self.work_area_offset {
|
||||
self.work_area_offset = new_rect;
|
||||
if let Err(error) = komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(monitor_index, new_rect),
|
||||
) {
|
||||
tracing::error!(
|
||||
"error applying work area offset to monitor '{}': {}",
|
||||
monitor_index,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.komorebi_notification_state.is_some() && !self.disabled {
|
||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
||||
self.disabled = true;
|
||||
} else {
|
||||
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
|
||||
self.disabled = true;
|
||||
}
|
||||
|
||||
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 {
|
||||
fn update_size_rect(&mut self) {
|
||||
let position = self.config.position.clone().unwrap_or(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
@@ -336,16 +398,26 @@ impl Komobar {
|
||||
}),
|
||||
});
|
||||
|
||||
let start = position.start.unwrap_or(Position {
|
||||
let mut start = position.start.unwrap_or(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
});
|
||||
|
||||
let end = position.end.unwrap_or(Position {
|
||||
let mut end = position.end.unwrap_or(Position {
|
||||
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
|
||||
y: BAR_HEIGHT,
|
||||
});
|
||||
|
||||
if let Some(height) = self.config.height {
|
||||
end.y = height;
|
||||
}
|
||||
|
||||
let margin = get_individual_spacing(0.0, &self.config.margin);
|
||||
|
||||
start.y += margin.top;
|
||||
start.x += margin.left;
|
||||
end.x -= margin.left + margin.right;
|
||||
|
||||
if end.y == 0.0 {
|
||||
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
|
||||
}
|
||||
@@ -358,16 +430,16 @@ impl Komobar {
|
||||
};
|
||||
}
|
||||
|
||||
fn try_apply_theme(&mut self, config: &KomobarConfig, ctx: &Context) {
|
||||
match config.theme {
|
||||
fn try_apply_theme(&mut self, ctx: &Context) {
|
||||
match self.config.theme {
|
||||
Some(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
theme,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
config.transparency_alpha,
|
||||
config.grouping,
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
@@ -385,8 +457,8 @@ impl Komobar {
|
||||
},
|
||||
);
|
||||
|
||||
let bar_transparency_alpha = config.transparency_alpha;
|
||||
let bar_grouping = config.grouping;
|
||||
let bar_transparency_alpha = self.config.transparency_alpha;
|
||||
let bar_grouping = self.config.grouping;
|
||||
let config = home_dir.join("komorebi.json");
|
||||
match komorebi_client::StaticConfig::read(&config) {
|
||||
Ok(config) => {
|
||||
@@ -446,13 +518,15 @@ impl Komobar {
|
||||
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
rx_gui: Receiver<KomorebiEvent>,
|
||||
rx_config: Receiver<KomobarConfig>,
|
||||
config: Arc<KomobarConfig>,
|
||||
config: KomobarConfig,
|
||||
) -> Self {
|
||||
let mut komobar = Self {
|
||||
hwnd: process_hwnd(),
|
||||
config: config.clone(),
|
||||
monitor_index: None,
|
||||
disabled: false,
|
||||
config,
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
komorebi_notification_state: None,
|
||||
left_widgets: vec![],
|
||||
@@ -464,12 +538,13 @@ impl Komobar {
|
||||
bg_color_with_alpha: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
|
||||
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
|
||||
size_rect: komorebi_client::Rect::default(),
|
||||
work_area_offset: komorebi_client::Rect::default(),
|
||||
applied_theme_on_first_frame: false,
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
komobar.apply_config(&cc.egui_ctx, None);
|
||||
// needs a double apply the first time for some reason
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
komobar.apply_config(&cc.egui_ctx, None);
|
||||
|
||||
komobar
|
||||
}
|
||||
@@ -503,6 +578,28 @@ impl Komobar {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
||||
|
||||
let mut fallbacks = HashMap::new();
|
||||
|
||||
fallbacks.insert("Microsoft YaHei", "C:\\Windows\\Fonts\\msyh.ttc"); // chinese
|
||||
fallbacks.insert("Malgun Gothic", "C:\\Windows\\Fonts\\malgun.ttf"); // korean
|
||||
fallbacks.insert("Leelawadee UI", "C:\\Windows\\Fonts\\LeelawUI.ttf"); // thai
|
||||
|
||||
for (name, path) in fallbacks {
|
||||
if let Ok(bytes) = std::fs::read(path) {
|
||||
fonts
|
||||
.font_data
|
||||
.insert(name.to_owned(), Arc::from(FontData::from_owned(bytes)));
|
||||
|
||||
for family in [FontFamily::Proportional, FontFamily::Monospace] {
|
||||
fonts
|
||||
.families
|
||||
.entry(family)
|
||||
.or_default()
|
||||
.insert(0, name.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let property = FontPropertyBuilder::new().family(name).build();
|
||||
|
||||
if let Some((font, _)) = system_fonts::get(&property) {
|
||||
@@ -510,20 +607,42 @@ impl Komobar {
|
||||
.font_data
|
||||
.insert(name.to_owned(), Arc::new(FontData::from_owned(font)));
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, name.to_owned());
|
||||
for family in [FontFamily::Proportional, FontFamily::Monospace] {
|
||||
fonts
|
||||
.families
|
||||
.entry(family)
|
||||
.or_default()
|
||||
.insert(0, name.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Monospace)
|
||||
.or_default()
|
||||
.push(name.to_owned());
|
||||
// Tell egui to use these fonts:
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
|
||||
// Tell egui to use these fonts:
|
||||
ctx.set_fonts(fonts);
|
||||
pub fn position_bar(&self) {
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
let window = komorebi_client::Window::from(hwnd);
|
||||
if let Err(error) = window.set_position(&self.size_rect, false) {
|
||||
tracing::error!("{}", error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_monitor_coordinates(&mut self, monitor_size: &komorebi_client::Rect) {
|
||||
// Store the new monitor coordinates
|
||||
MONITOR_TOP.store(monitor_size.top, Ordering::SeqCst);
|
||||
MONITOR_LEFT.store(monitor_size.left, Ordering::SeqCst);
|
||||
MONITOR_RIGHT.store(monitor_size.right, Ordering::SeqCst);
|
||||
|
||||
// Since the `config.position` is changed on `main.rs` we need to update it here.
|
||||
// If the user had set up some `start` position, that will be overriden here
|
||||
// since we have no way to know what was that value since it might have been
|
||||
// changed on `main.rs`. However if the users use the new configs this won't be
|
||||
// a problem for them.
|
||||
if let Some(start) = self.config.position.as_mut().and_then(|p| p.start.as_mut()) {
|
||||
start.x = monitor_size.left as f32;
|
||||
start.y = monitor_size.top as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,39 +659,154 @@ impl eframe::App for Komobar {
|
||||
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||
self.apply_config(
|
||||
ctx,
|
||||
&self.config.clone(),
|
||||
self.komorebi_notification_state.clone(),
|
||||
);
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.apply_config(
|
||||
ctx,
|
||||
&updated_config,
|
||||
self.komorebi_notification_state.clone(),
|
||||
);
|
||||
self.config = updated_config;
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.config.monitor.index,
|
||||
self.rx_gui.clone(),
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
match self.rx_gui.try_recv() {
|
||||
Err(error) => match error {
|
||||
TryRecvError::Empty => {}
|
||||
TryRecvError::Disconnected => {
|
||||
tracing::error!(
|
||||
"failed to receive komorebi notification on gui thread: {error}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(KomorebiEvent::Notification(notification)) => {
|
||||
let state = ¬ification.state;
|
||||
let usr_monitor_index = match &self.config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
};
|
||||
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
|
||||
self.monitor_index = monitor_index;
|
||||
let mut should_apply_config = false;
|
||||
|
||||
if self.monitor_index.is_none()
|
||||
|| self
|
||||
.monitor_index
|
||||
.is_some_and(|idx| idx >= state.monitors.elements().len())
|
||||
{
|
||||
if !self.disabled {
|
||||
// Monitor for this bar got disconnected lets disable the bar until it
|
||||
// reconnects
|
||||
self.disabled = true;
|
||||
tracing::warn!(
|
||||
"This bar's monitor got disconnected. The bar will be disabled until it reconnects..."
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if self.disabled {
|
||||
tracing::info!("Found this bar's monitor. The bar will be enabled!");
|
||||
|
||||
// Restore the bar in case it has been minimized when the monitor
|
||||
// disconnected
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
let window = komorebi_client::Window::from(hwnd);
|
||||
if window.is_miminized() {
|
||||
komorebi_client::WindowsApi::restore_window(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
self.disabled = false;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
notification.event,
|
||||
NotificationEvent::Monitor(MonitorNotification::DisplayConnectionChange)
|
||||
) {
|
||||
let monitor_index = self.monitor_index.expect("should have a monitor index");
|
||||
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
|
||||
if self.disabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if monitor coordinates/size has changed
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
let top = MONITOR_TOP.load(Ordering::SeqCst);
|
||||
let left = MONITOR_LEFT.load(Ordering::SeqCst);
|
||||
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
|
||||
let rect = komorebi_client::Rect {
|
||||
top,
|
||||
left,
|
||||
bottom: monitor_size.bottom,
|
||||
right,
|
||||
};
|
||||
if *monitor_size != rect {
|
||||
tracing::info!(
|
||||
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
|
||||
monitor_size
|
||||
);
|
||||
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.monitor_index,
|
||||
notification,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if should_apply_config {
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
|
||||
// Reposition the Bar
|
||||
self.position_bar();
|
||||
}
|
||||
}
|
||||
Ok(KomorebiEvent::Reconnect) => {
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
if let Err(error) = komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(monitor_index, self.work_area_offset),
|
||||
) {
|
||||
tracing::error!(
|
||||
"error applying work area offset to monitor '{}': {}",
|
||||
monitor_index,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
tracing::info!("work area offset applied to monitor: {}", monitor_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.disabled {
|
||||
// The check for disabled is performed above, if we get here and the bar is still
|
||||
// disabled then we should return without drawing anything.
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.applied_theme_on_first_frame {
|
||||
self.try_apply_theme(&self.config.clone(), ctx);
|
||||
self.try_apply_theme(ctx);
|
||||
self.applied_theme_on_first_frame = true;
|
||||
}
|
||||
|
||||
@@ -587,54 +821,82 @@ impl eframe::App for Komobar {
|
||||
};
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
self.position_bar();
|
||||
}
|
||||
}
|
||||
|
||||
let frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(
|
||||
frame.inner_margin.x,
|
||||
frame.inner_margin.y,
|
||||
))
|
||||
.fill(*self.bg_color_with_alpha.borrow())
|
||||
} else {
|
||||
Frame::none().fill(*self.bg_color_with_alpha.borrow())
|
||||
let frame = match &self.config.padding {
|
||||
None => {
|
||||
if let Some(frame) = &self.config.frame {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(
|
||||
frame.inner_margin.x,
|
||||
frame.inner_margin.y,
|
||||
))
|
||||
.fill(*self.bg_color_with_alpha.borrow())
|
||||
} else {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(0.0))
|
||||
.fill(*self.bg_color_with_alpha.borrow())
|
||||
}
|
||||
}
|
||||
Some(padding) => {
|
||||
let padding = padding.to_individual(DEFAULT_PADDING);
|
||||
Frame::none()
|
||||
.inner_margin(Margin {
|
||||
top: padding.top,
|
||||
bottom: padding.bottom,
|
||||
left: padding.left,
|
||||
right: padding.right,
|
||||
})
|
||||
.fill(*self.bg_color_with_alpha.borrow())
|
||||
}
|
||||
};
|
||||
|
||||
let mut render_config = self.render_config.borrow_mut();
|
||||
|
||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |_| {
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
// Apply grouping logic for the bar as a whole
|
||||
let area_frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::none().inner_margin(Margin::symmetric(0.0, frame.inner_margin.y))
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(0.0, frame.inner_margin.y))
|
||||
.outer_margin(Margin::same(0.0))
|
||||
} else {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(0.0))
|
||||
.outer_margin(Margin::same(0.0))
|
||||
};
|
||||
|
||||
let available_height = ui.max_rect().max.y;
|
||||
ctx.style_mut(|style| {
|
||||
style.spacing.interact_size.y = available_height;
|
||||
});
|
||||
|
||||
if !self.left_widgets.is_empty() {
|
||||
// Left-aligned widgets layout
|
||||
Area::new(Id::new("left_panel"))
|
||||
.anchor(Align2::LEFT_CENTER, [0.0, 0.0]) // Align in the left center of the window
|
||||
.show(ctx, |ui| {
|
||||
let mut left_area_frame = area_frame;
|
||||
if let Some(frame) = &self.config.frame {
|
||||
if let Some(padding) = self
|
||||
.config
|
||||
.padding
|
||||
.as_ref()
|
||||
.map(|s| s.to_individual(DEFAULT_PADDING))
|
||||
{
|
||||
left_area_frame.inner_margin.left = padding.left;
|
||||
left_area_frame.inner_margin.top = padding.top;
|
||||
left_area_frame.inner_margin.bottom = padding.bottom;
|
||||
} else if let Some(frame) = &self.config.frame {
|
||||
left_area_frame.inner_margin.left = frame.inner_margin.x;
|
||||
left_area_frame.inner_margin.top = frame.inner_margin.y;
|
||||
left_area_frame.inner_margin.bottom = frame.inner_margin.y;
|
||||
}
|
||||
|
||||
left_area_frame.show(ui, |ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let mut render_conf = render_config.clone();
|
||||
render_conf.alignment = Some(Alignment::Left);
|
||||
|
||||
@@ -654,20 +916,40 @@ impl eframe::App for Komobar {
|
||||
.anchor(Align2::RIGHT_CENTER, [0.0, 0.0]) // Align in the right center of the window
|
||||
.show(ctx, |ui| {
|
||||
let mut right_area_frame = area_frame;
|
||||
if let Some(frame) = &self.config.frame {
|
||||
if let Some(padding) = self
|
||||
.config
|
||||
.padding
|
||||
.as_ref()
|
||||
.map(|s| s.to_individual(DEFAULT_PADDING))
|
||||
{
|
||||
right_area_frame.inner_margin.right = padding.right;
|
||||
right_area_frame.inner_margin.top = padding.top;
|
||||
right_area_frame.inner_margin.bottom = padding.bottom;
|
||||
} else if let Some(frame) = &self.config.frame {
|
||||
right_area_frame.inner_margin.right = frame.inner_margin.x;
|
||||
right_area_frame.inner_margin.top = frame.inner_margin.y;
|
||||
right_area_frame.inner_margin.bottom = frame.inner_margin.y;
|
||||
}
|
||||
right_area_frame.show(ui, |ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let mut render_conf = render_config.clone();
|
||||
render_conf.alignment = Some(Alignment::Right);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
right_area_frame.show(ui, |ui| {
|
||||
let initial_size = Vec2 {
|
||||
x: ui.available_size_before_wrap().x,
|
||||
y: ui.spacing().interact_size.y,
|
||||
};
|
||||
ui.allocate_ui_with_layout(
|
||||
initial_size,
|
||||
Layout::right_to_left(Align::Center),
|
||||
|ui| {
|
||||
let mut render_conf = render_config.clone();
|
||||
render_conf.alignment = Some(Alignment::Right);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -677,9 +959,22 @@ impl eframe::App for Komobar {
|
||||
Area::new(Id::new("center_panel"))
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
|
||||
.show(ctx, |ui| {
|
||||
let center_area_frame = area_frame;
|
||||
let mut center_area_frame = area_frame;
|
||||
if let Some(padding) = self
|
||||
.config
|
||||
.padding
|
||||
.as_ref()
|
||||
.map(|s| s.to_individual(DEFAULT_PADDING))
|
||||
{
|
||||
center_area_frame.inner_margin.top = padding.top;
|
||||
center_area_frame.inner_margin.bottom = padding.bottom;
|
||||
} else if let Some(frame) = &self.config.frame {
|
||||
center_area_frame.inner_margin.top = frame.inner_margin.y;
|
||||
center_area_frame.inner_margin.bottom = frame.inner_margin.y;
|
||||
}
|
||||
|
||||
center_area_frame.show(ui, |ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let mut render_conf = render_config.clone();
|
||||
render_conf.alignment = Some(Alignment::Center);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::render::Grouping;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
@@ -12,15 +13,67 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.33`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.35`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
pub height: Option<f32>,
|
||||
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
|
||||
/// vertical definition which can each take a single value for a symmetric padding or two
|
||||
/// values for each side, i.e.:
|
||||
/// ```json
|
||||
/// "padding": {
|
||||
/// "horizontal": 10
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "padding": {
|
||||
/// "horizontal": [left, right]
|
||||
/// }
|
||||
/// ```
|
||||
/// You can also set individual padding on each side like this:
|
||||
/// ```json
|
||||
/// "padding": {
|
||||
/// "top": 10,
|
||||
/// "bottom": 10,
|
||||
/// "left": 10,
|
||||
/// "right": 10,
|
||||
/// }
|
||||
/// ```
|
||||
/// By default, padding is set to 10 on all sides.
|
||||
pub padding: Option<Padding>,
|
||||
/// Bar margin. Use one value for all sides or use a grouped margin for horizontal and/or
|
||||
/// vertical definition which can each take a single value for a symmetric margin or two
|
||||
/// values for each side, i.e.:
|
||||
/// ```json
|
||||
/// "margin": {
|
||||
/// "horizontal": 10
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "margin": {
|
||||
/// "vertical": [top, bottom]
|
||||
/// }
|
||||
/// ```
|
||||
/// You can also set individual margin on each side like this:
|
||||
/// ```json
|
||||
/// "margin": {
|
||||
/// "top": 10,
|
||||
/// "bottom": 10,
|
||||
/// "left": 10,
|
||||
/// "right": 10,
|
||||
/// }
|
||||
/// ```
|
||||
/// By default, margin is set to 0 on all sides.
|
||||
pub margin: Option<Margin>,
|
||||
/// Bar positioning options
|
||||
#[serde(alias = "viewport")]
|
||||
pub position: Option<PositionConfig>,
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// Monitor options
|
||||
pub monitor: MonitorConfig,
|
||||
/// The monitor index or the full monitor options
|
||||
pub monitor: MonitorConfigOrIndex,
|
||||
/// Font family
|
||||
pub font_family: Option<String>,
|
||||
/// Font size (default: 12.5)
|
||||
@@ -90,6 +143,15 @@ pub struct FrameConfig {
|
||||
pub inner_margin: Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum MonitorConfigOrIndex {
|
||||
/// The monitor index where you want the bar to show
|
||||
Index(usize),
|
||||
/// The full monitor options with the index and an optional work_area_offset
|
||||
MonitorConfig(MonitorConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MonitorConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
@@ -98,6 +160,154 @@ pub struct MonitorConfig {
|
||||
pub work_area_offset: Option<Rect>,
|
||||
}
|
||||
|
||||
pub type Padding = SpacingKind;
|
||||
pub type Margin = SpacingKind;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
// WARNING: To any developer messing with this code in the future: The order here matters!
|
||||
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
|
||||
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
|
||||
// individual values.
|
||||
pub enum SpacingKind {
|
||||
All(f32),
|
||||
Individual(IndividualSpacingConfig),
|
||||
Grouped(GroupedSpacingConfig),
|
||||
}
|
||||
|
||||
impl SpacingKind {
|
||||
pub fn to_individual(&self, default: f32) -> IndividualSpacingConfig {
|
||||
match self {
|
||||
SpacingKind::All(m) => IndividualSpacingConfig::all(*m),
|
||||
SpacingKind::Grouped(grouped_spacing_config) => {
|
||||
let vm = grouped_spacing_config.vertical.as_ref().map_or(
|
||||
IndividualSpacingConfig::vertical(default),
|
||||
|vm| match vm {
|
||||
GroupedSpacingOptions::Symmetrical(m) => {
|
||||
IndividualSpacingConfig::vertical(*m)
|
||||
}
|
||||
GroupedSpacingOptions::Split(tm, bm) => {
|
||||
IndividualSpacingConfig::vertical(*tm).bottom(*bm)
|
||||
}
|
||||
},
|
||||
);
|
||||
let hm = grouped_spacing_config.horizontal.as_ref().map_or(
|
||||
IndividualSpacingConfig::horizontal(default),
|
||||
|hm| match hm {
|
||||
GroupedSpacingOptions::Symmetrical(m) => {
|
||||
IndividualSpacingConfig::horizontal(*m)
|
||||
}
|
||||
GroupedSpacingOptions::Split(lm, rm) => {
|
||||
IndividualSpacingConfig::horizontal(*lm).right(*rm)
|
||||
}
|
||||
},
|
||||
);
|
||||
IndividualSpacingConfig {
|
||||
top: vm.top,
|
||||
bottom: vm.bottom,
|
||||
left: hm.left,
|
||||
right: hm.right,
|
||||
}
|
||||
}
|
||||
SpacingKind::Individual(m) => *m,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GroupedSpacingConfig {
|
||||
pub vertical: Option<GroupedSpacingOptions>,
|
||||
pub horizontal: Option<GroupedSpacingOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum GroupedSpacingOptions {
|
||||
Symmetrical(f32),
|
||||
Split(f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IndividualSpacingConfig {
|
||||
pub top: f32,
|
||||
pub bottom: f32,
|
||||
pub left: f32,
|
||||
pub right: f32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl IndividualSpacingConfig {
|
||||
pub const ZERO: Self = IndividualSpacingConfig {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
};
|
||||
|
||||
pub fn all(value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
top: value,
|
||||
bottom: value,
|
||||
left: value,
|
||||
right: value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal(value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
left: value,
|
||||
right: value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
top: value,
|
||||
bottom: value,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn top(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig { top: value, ..self }
|
||||
}
|
||||
|
||||
pub fn bottom(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
bottom: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
left: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn right(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
right: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_individual_spacing(
|
||||
default: f32,
|
||||
spacing: &Option<SpacingKind>,
|
||||
) -> IndividualSpacingConfig {
|
||||
spacing
|
||||
.as_ref()
|
||||
.map_or(IndividualSpacingConfig::all(default), |s| {
|
||||
s.to_individual(default)
|
||||
})
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
@@ -108,7 +318,10 @@ impl KomobarConfig {
|
||||
|
||||
if value.frame.is_none() {
|
||||
value.frame = Some(FrameConfig {
|
||||
inner_margin: Position { x: 10.0, y: 10.0 },
|
||||
inner_margin: Position {
|
||||
x: DEFAULT_PADDING,
|
||||
y: DEFAULT_PADDING,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,7 +366,7 @@ pub enum KomobarTheme {
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
|
||||
@@ -13,6 +13,48 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Custom format with additive modifiers for integer format specifiers
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CustomModifiers {
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
format: String,
|
||||
/// Additive modifiers for integer format specifiers (e.g. { "%U": 1 } to increment the zero-indexed week number by 1)
|
||||
modifiers: std::collections::HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl CustomModifiers {
|
||||
fn apply(&self, output: &str) -> String {
|
||||
let int_formatters = vec![
|
||||
"%Y", "%C", "%y", "%m", "%d", "%e", "%w", "%u", "%U", "%W", "%G", "%g", "%V", "%j",
|
||||
"%H", "%k", "%I", "%l", "%M", "%S", "%f",
|
||||
];
|
||||
|
||||
let mut modified_output = output.to_string();
|
||||
|
||||
for (modifier, value) in &self.modifiers {
|
||||
// check if formatter is integer type
|
||||
if !int_formatters.contains(&modifier.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the strftime value of modifier
|
||||
let formatted_modifier = chrono::Local::now().format(modifier).to_string();
|
||||
|
||||
// find the gotten value in the original output
|
||||
if let Some(pos) = modified_output.find(&formatted_modifier) {
|
||||
let start = pos;
|
||||
let end = start + formatted_modifier.len();
|
||||
// replace that value with the modified value
|
||||
if let Ok(num) = formatted_modifier.parse::<i32>() {
|
||||
modified_output.replace_range(start..end, &(num + value).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modified_output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
@@ -45,6 +87,8 @@ pub enum DateFormat {
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
/// Custom format with modifiers
|
||||
CustomModifiers(CustomModifiers),
|
||||
}
|
||||
|
||||
impl DateFormat {
|
||||
@@ -58,13 +102,14 @@ impl DateFormat {
|
||||
};
|
||||
}
|
||||
|
||||
fn fmt_string(&self) -> String {
|
||||
pub fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
DateFormat::MonthDateYear => String::from("%D"),
|
||||
DateFormat::YearMonthDate => String::from("%F"),
|
||||
DateFormat::DateMonthYear => String::from("%v"),
|
||||
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
|
||||
DateFormat::Custom(custom) => custom.to_string(),
|
||||
DateFormat::CustomModifiers(custom) => custom.format.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,9 +123,15 @@ pub struct Date {
|
||||
|
||||
impl Date {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
let formatted = chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.to_string();
|
||||
|
||||
// if custom modifiers are used, apply them
|
||||
match &self.format {
|
||||
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
|
||||
_ => formatted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
177
komorebi-bar/src/keyboard.rs
Executable file
177
komorebi-bar/src/keyboard.rs
Executable file
@@ -0,0 +1,177 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows::Win32::Globalization::LCIDToLocaleName;
|
||||
use windows::Win32::Globalization::LOCALE_ALLOW_NEUTRAL_NAMES;
|
||||
use windows::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
|
||||
const ERROR_TEXT: &str = "Error";
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KeyboardConfig {
|
||||
/// Enable the Input widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 1 second)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<KeyboardConfig> for Keyboard {
|
||||
fn from(value: KeyboardConfig) -> Self {
|
||||
let data_refresh_interval = value
|
||||
.data_refresh_interval
|
||||
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
lang_name: get_lang(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Keyboard {
|
||||
pub enable: bool,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
lang_name: String,
|
||||
}
|
||||
|
||||
/// Retrieves the name of the active keyboard layout for the current foreground window.
|
||||
///
|
||||
/// This function determines the active keyboard layout by querying the system for the
|
||||
/// foreground window's thread ID and its associated keyboard layout. It then attempts
|
||||
/// to retrieve the locale name corresponding to the keyboard layout.
|
||||
///
|
||||
/// # Failure Cases
|
||||
///
|
||||
/// This function can fail in two distinct scenarios:
|
||||
///
|
||||
/// 1. **Failure to Retrieve the Locale Name**:
|
||||
/// If the system fails to retrieve the locale name (e.g., due to an invalid or unsupported
|
||||
/// language identifier), the function will return `Err(())`.
|
||||
///
|
||||
/// 2. **Invalid UTF-16 Characters in the Locale Name**:
|
||||
/// If the retrieved locale name contains invalid UTF-16 sequences, the conversion to a Rust
|
||||
/// `String` will fail, and the function will return `Err(())`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
|
||||
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
|
||||
/// invalid UTF-16 characters during conversion.
|
||||
fn get_active_keyboard_layout() -> Result<String, ()> {
|
||||
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
|
||||
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
|
||||
|
||||
// Extract the low word (language identifier) from the keyboard layout handle.
|
||||
let lang_id = (lcid.0 as u32) & 0xFFFF;
|
||||
let mut locale_name_buffer = [0; LOCALE_NAME_MAX_LENGTH as usize];
|
||||
let char_count = unsafe {
|
||||
LCIDToLocaleName(
|
||||
lang_id,
|
||||
Some(&mut locale_name_buffer),
|
||||
LOCALE_ALLOW_NEUTRAL_NAMES,
|
||||
)
|
||||
};
|
||||
|
||||
match char_count {
|
||||
0 => Err(()),
|
||||
_ => String::from_utf16(&locale_name_buffer[..char_count as usize]).map_err(|_| ()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the name of the active keyboard layout or a fallback error message.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// - **Success Case**:
|
||||
/// If [`get_active_keyboard_layout`] succeeds, this function returns the retrieved keyboard
|
||||
/// layout name as a `String`.
|
||||
///
|
||||
/// - **Failure Case**:
|
||||
/// If [`get_active_keyboard_layout`] fails, this function returns the value of `ERROR_TEXT`
|
||||
/// as a fallback message. This ensures that the function always returns a valid `String`,
|
||||
/// even in error scenarios.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `String` representing either:
|
||||
/// - The name of the active keyboard layout, or
|
||||
/// - The fallback error message (`ERROR_TEXT`) if the layout name cannot be retrieved.
|
||||
fn get_lang() -> String {
|
||||
get_active_keyboard_layout()
|
||||
.map(|l| l.trim_end_matches('\0').to_string())
|
||||
.unwrap_or_else(|_| ERROR_TEXT.to_string())
|
||||
}
|
||||
|
||||
impl Keyboard {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.last_updated = now;
|
||||
self.lang_name = get_lang();
|
||||
}
|
||||
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("KB: {}", self.lang_name),
|
||||
LabelPrefix::None | LabelPrefix::Icon => self.lang_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Keyboard {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::KEYBOARD.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ use crate::widget::BarWidget;
|
||||
use crate::ICON_CACHE;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_INDEX;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
@@ -30,15 +28,18 @@ use eframe::egui::Vec2;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::Container;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::Window;
|
||||
use komorebi_client::Workspace;
|
||||
use komorebi_client::WorkspaceLayer;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -49,6 +50,8 @@ pub struct KomorebiConfig {
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
/// Configure the Layout widget
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
/// Configure the Workspace Layer widget
|
||||
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
|
||||
/// Configure the Focused Window widget
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
/// Configure the Configuration Switcher widget
|
||||
@@ -75,6 +78,12 @@ pub struct KomorebiLayoutConfig {
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiWorkspaceLayerConfig {
|
||||
/// Enable the Komorebi Workspace Layer widget
|
||||
pub enable: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiFocusedWindowConfig {
|
||||
/// Enable the Komorebi Focused Window widget
|
||||
@@ -99,7 +108,7 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
if let Some(configuration_switcher) = &value.configuration_switcher {
|
||||
let mut configuration_switcher = configuration_switcher.clone();
|
||||
for (_, location) in configuration_switcher.configurations.iter_mut() {
|
||||
*location = dunce::simplified(&PathBuf::from(location.clone()))
|
||||
*location = dunce::simplified(&PathBuf::from(location.clone()).replace_env())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
@@ -122,10 +131,12 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
|
||||
stack_accent: None,
|
||||
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
|
||||
monitor_usr_idx_map: HashMap::new(),
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout.clone(),
|
||||
focused_window: value.focused_window,
|
||||
workspace_layer: value.workspace_layer,
|
||||
configuration_switcher,
|
||||
}
|
||||
}
|
||||
@@ -137,6 +148,7 @@ pub struct Komorebi {
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
@@ -153,7 +165,7 @@ impl BarWidget for Komorebi {
|
||||
let format = workspaces.display.unwrap_or(DisplayFormat::Text);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, container_information)) in
|
||||
for (i, (ws, container_information, _)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
if SelectableFrame::new(
|
||||
@@ -280,6 +292,42 @@ impl BarWidget for Komorebi {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(layer_config) = &self.workspace_layer {
|
||||
if layer_config.enable {
|
||||
let layer = komorebi_notification_state
|
||||
.workspaces
|
||||
.iter()
|
||||
.find(|o| komorebi_notification_state.selected_workspace.eq(&o.0))
|
||||
.map(|(_, _, layer)| layer);
|
||||
|
||||
if let Some(layer) = layer {
|
||||
let name = layer.to_string();
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(name).selectable(false)))
|
||||
.clicked()
|
||||
&& komorebi_client::send_batch([
|
||||
SocketMessage::MouseFollowsFocus(false),
|
||||
SocketMessage::ToggleWorkspaceLayer,
|
||||
SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
),
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n\
|
||||
MouseFollowsFocus(false),
|
||||
ToggleWorkspaceLayer,
|
||||
MouseFollowsFocus({})",
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(layout_config) = &self.layout {
|
||||
if layout_config.enable {
|
||||
let workspace_idx: Option<usize> = komorebi_notification_state
|
||||
@@ -475,7 +523,11 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>,
|
||||
pub workspaces: Vec<(
|
||||
String,
|
||||
KomorebiNotificationStateContainerInformation,
|
||||
WorkspaceLayer,
|
||||
)>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: KomorebiNotificationStateContainerInformation,
|
||||
pub layout: KomorebiLayout,
|
||||
@@ -484,6 +536,7 @@ pub struct KomorebiNotificationState {
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub stack_accent: Option<Color32>,
|
||||
pub monitor_index: usize,
|
||||
pub monitor_usr_idx_map: HashMap<usize, usize>,
|
||||
}
|
||||
|
||||
impl KomorebiNotificationState {
|
||||
@@ -495,8 +548,8 @@ impl KomorebiNotificationState {
|
||||
pub fn handle_notification(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
monitor_index: usize,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
monitor_index: Option<usize>,
|
||||
notification: komorebi_client::Notification,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
@@ -504,119 +557,116 @@ impl KomorebiNotificationState {
|
||||
default_theme: Option<KomobarTheme>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
match rx_gui.try_recv() {
|
||||
Err(error) => match error {
|
||||
TryRecvError::Empty => {}
|
||||
TryRecvError::Disconnected => {
|
||||
tracing::error!(
|
||||
"failed to receive komorebi notification on gui thread: {error}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(notification) => {
|
||||
match notification.event {
|
||||
NotificationEvent::WindowManager(_) => {}
|
||||
NotificationEvent::Socket(message) => match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("removed theme from updated komorebi.json and applied default theme");
|
||||
} else {
|
||||
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
match notification.event {
|
||||
NotificationEvent::WindowManager(_) => {}
|
||||
NotificationEvent::Monitor(_) => {}
|
||||
NotificationEvent::Socket(message) => match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("removed theme from updated komorebi.json and applied default theme");
|
||||
} else {
|
||||
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let mut workspaces = vec![];
|
||||
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_show = if self.hide_empty_workspaces {
|
||||
focused_workspace_idx == i || !ws.containers().is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_show {
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
ws.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.workspaces = workspaces;
|
||||
|
||||
if monitor.workspaces()[focused_workspace_idx]
|
||||
.monocle_container()
|
||||
.is_some()
|
||||
{
|
||||
self.layout = KomorebiLayout::Monocle;
|
||||
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
} else if notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
} else {
|
||||
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
|
||||
komorebi_client::Layout::Default(layout) => {
|
||||
KomorebiLayout::Default(*layout)
|
||||
}
|
||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||
};
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
self.focused_container_information =
|
||||
(&monitor.workspaces()[focused_workspace_idx]).into();
|
||||
self.monitor_usr_idx_map = notification.state.monitor_usr_idx_map.clone();
|
||||
|
||||
if monitor_index.is_none()
|
||||
|| monitor_index.is_some_and(|idx| idx >= notification.state.monitors.elements().len())
|
||||
{
|
||||
// The bar's monitor is diconnected, so the bar is disabled no need to check anything
|
||||
// any further otherwise we'll get `OutOfBounds` panics.
|
||||
return;
|
||||
}
|
||||
let monitor_index = monitor_index.expect("should have a monitor index");
|
||||
self.monitor_index = monitor_index;
|
||||
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let mut workspaces = vec![];
|
||||
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_show = if self.hide_empty_workspaces {
|
||||
focused_workspace_idx == i || !ws.is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_show {
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
ws.into(),
|
||||
ws.layer().to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.workspaces = workspaces;
|
||||
|
||||
if monitor.workspaces()[focused_workspace_idx]
|
||||
.monocle_container()
|
||||
.is_some()
|
||||
{
|
||||
self.layout = KomorebiLayout::Monocle;
|
||||
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
} else if notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
} else {
|
||||
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
|
||||
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
|
||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||
};
|
||||
}
|
||||
|
||||
self.focused_container_information = (&monitor.workspaces()[focused_workspace_idx]).into();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod keyboard;
|
||||
mod komorebi;
|
||||
mod komorebi_layout;
|
||||
mod media;
|
||||
@@ -21,6 +22,7 @@ use crate::config::KomobarConfig;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use clap::Parser;
|
||||
use config::MonitorConfigOrIndex;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
@@ -36,12 +38,10 @@ use std::path::PathBuf;
|
||||
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;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
@@ -50,6 +50,7 @@ use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
use windows_core::BOOL;
|
||||
|
||||
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
||||
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
||||
@@ -57,6 +58,7 @@ pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static BAR_HEIGHT: f32 = 50.0;
|
||||
pub static DEFAULT_PADDING: f32 = 10.0;
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
@@ -113,6 +115,11 @@ fn process_hwnd() -> Option<isize> {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum KomorebiEvent {
|
||||
Notification(komorebi_client::Notification),
|
||||
Reconnect,
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
|
||||
|
||||
@@ -230,32 +237,43 @@ fn main() -> color_eyre::Result<()> {
|
||||
&SocketMessage::State,
|
||||
)?)?;
|
||||
|
||||
let (usr_monitor_index, work_area_offset) = match &config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
||||
(monitor_config.index, monitor_config.work_area_offset)
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
};
|
||||
let monitor_index = state
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.map_or(usr_monitor_index, |i| *i);
|
||||
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[config.monitor.index].size().right,
|
||||
state.monitors.elements()[monitor_index].size().right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().top,
|
||||
state.monitors.elements()[monitor_index].size().top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().left,
|
||||
MONITOR_LEFT.store(
|
||||
state.monitors.elements()[monitor_index].size().left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_INDEX.store(config.monitor.index, Ordering::SeqCst);
|
||||
MONITOR_INDEX.store(monitor_index, Ordering::SeqCst);
|
||||
|
||||
match config.position {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
@@ -263,14 +281,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if position.end.is_none() {
|
||||
position.end = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
@@ -287,15 +305,9 @@ fn main() -> color_eyre::Result<()> {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(rect) = &config.monitor.work_area_offset {
|
||||
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
|
||||
config.monitor.index,
|
||||
*rect,
|
||||
))?;
|
||||
tracing::info!(
|
||||
"work area offset applied to monitor: {}",
|
||||
config.monitor.index
|
||||
);
|
||||
if let Some(rect) = &work_area_offset {
|
||||
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(monitor_index, *rect))?;
|
||||
tracing::info!("work area offset applied to monitor: {}", monitor_index);
|
||||
}
|
||||
|
||||
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
|
||||
@@ -325,13 +337,10 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
tracing::info!("watching configuration file for changes");
|
||||
|
||||
let config_arc = Arc::new(config);
|
||||
eframe::run_native(
|
||||
"komorebi-bar",
|
||||
native_options,
|
||||
Box::new(|cc| {
|
||||
let config_cl = config_arc.clone();
|
||||
|
||||
let ctx_repainter = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
@@ -374,18 +383,12 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
tracing::info!("reconnected to komorebi");
|
||||
|
||||
if let Some(rect) = &config_cl.monitor.work_area_offset {
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
config_cl.monitor.index,
|
||||
*rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Reconnect) {
|
||||
tracing::error!("could not send komorebi reconnect event to gui thread: {error}")
|
||||
}
|
||||
|
||||
ctx_komorebi.request_repaint();
|
||||
continue;
|
||||
}
|
||||
|
||||
match String::from_utf8(buffer) {
|
||||
@@ -396,7 +399,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(notification) {
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
@@ -421,7 +424,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
|
||||
}),
|
||||
)
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
|
||||
@@ -148,7 +148,7 @@ impl Network {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s | ",
|
||||
"{: >width$}/s ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
@@ -159,14 +159,14 @@ impl Network {
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} | ", reading.received_text),
|
||||
format!("{} ", reading.received_text),
|
||||
reading.transmitted_text,
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s | ",
|
||||
"DOWN: {: >width$}/s ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
@@ -177,7 +177,7 @@ impl Network {
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s | ", reading.received_text),
|
||||
format!("\u{2211}DOWN: {}/s ", reading.received_text),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted_text),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -81,8 +82,13 @@ impl RenderExt for &KomobarConfig {
|
||||
let mut icon_font_id = text_font_id.clone();
|
||||
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
||||
|
||||
let monitor_idx = match &self.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
};
|
||||
|
||||
RenderConfig {
|
||||
monitor_idx: self.monitor.index,
|
||||
monitor_idx,
|
||||
spacing: self.widget_spacing.unwrap_or(10.0),
|
||||
grouping: self.grouping.unwrap_or(Grouping::None),
|
||||
background_color,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
@@ -6,8 +7,12 @@ use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -36,8 +41,16 @@ impl From<TimeConfig> for Time {
|
||||
pub enum TimeFormat {
|
||||
/// Twelve-hour format (with seconds)
|
||||
TwelveHour,
|
||||
/// Twelve-hour format (without seconds)
|
||||
TwelveHourWithoutSeconds,
|
||||
/// Twenty-four-hour format (with seconds)
|
||||
TwentyFourHour,
|
||||
/// Twenty-four-hour format (without seconds)
|
||||
TwentyFourHourWithoutSeconds,
|
||||
/// Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
||||
BinaryCircle,
|
||||
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
||||
BinaryRectangle,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
@@ -45,8 +58,12 @@ pub enum TimeFormat {
|
||||
impl TimeFormat {
|
||||
pub fn toggle(&mut self) {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwelveHourWithoutSeconds,
|
||||
TimeFormat::TwelveHourWithoutSeconds => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwentyFourHourWithoutSeconds,
|
||||
TimeFormat::TwentyFourHourWithoutSeconds => *self = TimeFormat::BinaryCircle,
|
||||
TimeFormat::BinaryCircle => *self = TimeFormat::BinaryRectangle,
|
||||
TimeFormat::BinaryRectangle => *self = TimeFormat::TwelveHour,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -54,7 +71,11 @@ impl TimeFormat {
|
||||
fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
|
||||
TimeFormat::TwelveHourWithoutSeconds => String::from("%l:%M %p"),
|
||||
TimeFormat::TwentyFourHour => String::from("%T"),
|
||||
TimeFormat::TwentyFourHourWithoutSeconds => String::from("%H:%M"),
|
||||
TimeFormat::BinaryCircle => String::from("c%T"),
|
||||
TimeFormat::BinaryRectangle => String::from("r%T"),
|
||||
TimeFormat::Custom(format) => format.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -72,6 +93,169 @@ impl Time {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn paint_binary_circle(
|
||||
&mut self,
|
||||
size: f32,
|
||||
number: u32,
|
||||
max_power: usize,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
) {
|
||||
let full_height = size;
|
||||
let height = full_height / 4.0;
|
||||
let width = height;
|
||||
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
|
||||
let color = ctx.style().visuals.text_color();
|
||||
|
||||
let c = response.rect.center();
|
||||
let r = height / 2.0 - 0.5;
|
||||
|
||||
if number == 1 || number == 3 || number == 5 || number == 7 || number == 9 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r, color);
|
||||
} else {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 2 || number == 3 || number == 6 || number == 7 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r, color);
|
||||
} else {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 4 || number == 5 || number == 6 || number == 7 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r, color);
|
||||
} else if max_power > 2 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 8 || number == 9 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r, color);
|
||||
} else if max_power > 3 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r / 2.5, color);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_binary_rect(
|
||||
&mut self,
|
||||
size: f32,
|
||||
number: u32,
|
||||
max_power: usize,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
) {
|
||||
let full_height = size;
|
||||
let height = full_height / 4.0;
|
||||
let width = height * 1.5;
|
||||
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
|
||||
let color = ctx.style().visuals.text_color();
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
|
||||
let round_all = Rounding::same(response.rect.width() * 0.1);
|
||||
let round_top = Rounding {
|
||||
nw: round_all.nw,
|
||||
ne: round_all.ne,
|
||||
..Default::default()
|
||||
};
|
||||
let round_none = Rounding::ZERO;
|
||||
let round_bottom = Rounding {
|
||||
sw: round_all.nw,
|
||||
se: round_all.ne,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if max_power == 2 {
|
||||
let mut rect = response.rect.shrink(stroke.width);
|
||||
rect.set_height(rect.height() - height * 2.0);
|
||||
rect = rect.translate(Vec2::new(0.0, height * 2.0));
|
||||
painter.rect_stroke(rect, round_all, stroke);
|
||||
} else if max_power == 3 {
|
||||
let mut rect = response.rect.shrink(stroke.width);
|
||||
rect.set_height(rect.height() - height);
|
||||
rect = rect.translate(Vec2::new(0.0, height));
|
||||
painter.rect_stroke(rect, round_all, stroke);
|
||||
} else {
|
||||
painter.rect_stroke(response.rect.shrink(stroke.width), round_all, stroke);
|
||||
}
|
||||
|
||||
let mut rect_bin = response.rect;
|
||||
rect_bin.set_width(width);
|
||||
|
||||
if number == 1 || number == 5 || number == 9 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 3.0)),
|
||||
round_bottom,
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 2 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
|
||||
if max_power == 2 {
|
||||
round_top
|
||||
} else {
|
||||
round_none
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 3 {
|
||||
rect_bin.set_height(height * 2.0);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
|
||||
round_bottom,
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 4 || number == 5 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
|
||||
if max_power == 3 {
|
||||
round_top
|
||||
} else {
|
||||
round_none
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 6 {
|
||||
rect_bin.set_height(height * 2.0);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
|
||||
if max_power == 3 {
|
||||
round_top
|
||||
} else {
|
||||
round_none
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 7 {
|
||||
rect_bin.set_height(height * 3.0);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height)),
|
||||
if max_power == 3 {
|
||||
round_all
|
||||
} else {
|
||||
round_bottom
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 8 || number == 9 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(rect_bin.translate(Vec2::new(0.0, 0.0)), round_top, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +264,9 @@ impl BarWidget for Time {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let use_binary_circle = output.starts_with('c');
|
||||
let use_binary_rectangle = output.starts_with('r');
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -96,20 +283,83 @@ impl BarWidget for Time {
|
||||
output.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if !use_binary_circle && !use_binary_rectangle {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let font_id = config.icon_font_id.clone();
|
||||
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.show(ui, |ui| {
|
||||
if !is_reversed {
|
||||
ui.add(Label::new(layout_job.clone()).selectable(false));
|
||||
}
|
||||
|
||||
if use_binary_circle || use_binary_rectangle {
|
||||
let ordered_output = if is_reversed {
|
||||
output.chars().rev().collect()
|
||||
} else {
|
||||
output
|
||||
};
|
||||
|
||||
for (section_index, section) in
|
||||
ordered_output.split(':').enumerate()
|
||||
{
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::splat(2.0);
|
||||
for (number_index, number_char) in
|
||||
section.chars().enumerate()
|
||||
{
|
||||
if let Some(number) = number_char.to_digit(10) {
|
||||
// the hour is the second char in the first section (in reverse, it's in the last section)
|
||||
let max_power = match (
|
||||
is_reversed,
|
||||
section_index,
|
||||
number_index,
|
||||
) {
|
||||
(true, 2, 1) | (false, 0, 1) => 2,
|
||||
(true, _, 1) | (false, _, 0) => 3,
|
||||
_ => 4,
|
||||
};
|
||||
|
||||
if use_binary_circle {
|
||||
self.paint_binary_circle(
|
||||
font_id.size,
|
||||
number,
|
||||
max_power,
|
||||
ctx,
|
||||
ui,
|
||||
);
|
||||
} else if use_binary_rectangle {
|
||||
self.paint_binary_rect(
|
||||
font_id.size,
|
||||
number,
|
||||
max_power,
|
||||
ctx,
|
||||
ui,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if is_reversed {
|
||||
ui.add(Label::new(layout_job.clone()).selectable(false));
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
|
||||
@@ -4,6 +4,8 @@ use crate::cpu::Cpu;
|
||||
use crate::cpu::CpuConfig;
|
||||
use crate::date::Date;
|
||||
use crate::date::DateConfig;
|
||||
use crate::keyboard::Keyboard;
|
||||
use crate::keyboard::KeyboardConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiConfig;
|
||||
use crate::media::Media;
|
||||
@@ -34,6 +36,7 @@ pub enum WidgetConfig {
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
Keyboard(KeyboardConfig),
|
||||
Komorebi(KomorebiConfig),
|
||||
Media(MediaConfig),
|
||||
Memory(MemoryConfig),
|
||||
@@ -49,6 +52,7 @@ impl WidgetConfig {
|
||||
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
||||
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
||||
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
||||
WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
|
||||
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
|
||||
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||
@@ -64,6 +68,7 @@ impl WidgetConfig {
|
||||
WidgetConfig::Battery(config) => config.enable,
|
||||
WidgetConfig::Cpu(config) => config.enable,
|
||||
WidgetConfig::Date(config) => config.enable,
|
||||
WidgetConfig::Keyboard(config) => config.enable,
|
||||
WidgetConfig::Komorebi(config) => {
|
||||
config.workspaces.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.layout.as_ref().is_some_and(|w| w.enable)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::animation::PerAnimationPrefixConfig;
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
pub use komorebi::config_generation::IdWithIdentifier;
|
||||
pub use komorebi::config_generation::IdWithIdentifierAndComment;
|
||||
pub use komorebi::config_generation::MatchingRule;
|
||||
pub use komorebi::config_generation::MatchingStrategy;
|
||||
pub use komorebi::container::Container;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::resolve_home_path;
|
||||
@@ -15,6 +20,10 @@ pub use komorebi::core::Arrangement;
|
||||
pub use komorebi::core::Axis;
|
||||
pub use komorebi::core::BorderImplementation;
|
||||
pub use komorebi::core::BorderStyle;
|
||||
pub use komorebi::core::Column;
|
||||
pub use komorebi::core::ColumnSplit;
|
||||
pub use komorebi::core::ColumnSplitWithCapacity;
|
||||
pub use komorebi::core::ColumnWidth;
|
||||
pub use komorebi::core::CustomLayout;
|
||||
pub use komorebi::core::CycleDirection;
|
||||
pub use komorebi::core::DefaultLayout;
|
||||
@@ -25,6 +34,7 @@ pub use komorebi::core::Layout;
|
||||
pub use komorebi::core::MoveBehaviour;
|
||||
pub use komorebi::core::OperationBehaviour;
|
||||
pub use komorebi::core::OperationDirection;
|
||||
pub use komorebi::core::PathExt;
|
||||
pub use komorebi::core::Rect;
|
||||
pub use komorebi::core::Sizing;
|
||||
pub use komorebi::core::SocketMessage;
|
||||
@@ -33,21 +43,31 @@ pub use komorebi::core::StackbarMode;
|
||||
pub use komorebi::core::StateQuery;
|
||||
pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::window::Window;
|
||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::workspace::WorkspaceLayer;
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -10,6 +10,7 @@ komorebi-client = { path = "../komorebi-client" }
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
random_word = { version = "0.4.3", features = ["en"] }
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
@@ -215,7 +215,7 @@ impl KomorebiGui {
|
||||
extern "system" fn enum_window(
|
||||
hwnd: windows::Win32::Foundation::HWND,
|
||||
lparam: windows::Win32::Foundation::LPARAM,
|
||||
) -> windows::Win32::Foundation::BOOL {
|
||||
) -> windows_core::BOOL {
|
||||
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
|
||||
let window = Window::from(hwnd.0 as isize);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "24362c4" }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "911079d" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f85cc3c", default-features = false, features = ["egui30"] }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_variant = "0.1"
|
||||
strum = "0.26"
|
||||
strum = "0.26"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub use base16_egui_themes::Base16;
|
||||
@@ -11,7 +12,7 @@ pub use catppuccin_egui;
|
||||
pub use eframe::egui::Color32;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
@@ -48,7 +49,7 @@ impl Theme {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
pub enum Base16Value {
|
||||
Base00,
|
||||
Base01,
|
||||
@@ -92,7 +93,7 @@ impl Base16Value {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
pub enum Catppuccin {
|
||||
Frappe,
|
||||
Latte,
|
||||
@@ -117,7 +118,7 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
pub enum CatppuccinValue {
|
||||
Rosewater,
|
||||
Flamingo,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
@@ -25,7 +25,7 @@ lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.8"
|
||||
os_info = "3.10"
|
||||
parking_lot = "0.12"
|
||||
paste = { workspace = true }
|
||||
regex = "1"
|
||||
@@ -44,13 +44,17 @@ which = { workspace = true }
|
||||
win32-display-data = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-numerics = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.53"
|
||||
winreg = "0.55"
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
|
||||
[features]
|
||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||
|
||||
@@ -22,7 +22,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
Prefix(HashMap<AnimationPrefix, T>),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
@@ -17,8 +18,6 @@ use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Foundation::Numerics::Matrix3x2;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
@@ -32,7 +31,6 @@ use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||
@@ -68,13 +66,29 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows_core::BOOL;
|
||||
use windows_core::PCWSTR;
|
||||
use windows_numerics::Matrix3x2;
|
||||
|
||||
pub struct RenderFactory(ID2D1Factory);
|
||||
unsafe impl Sync for RenderFactory {}
|
||||
unsafe impl Send for RenderFactory {}
|
||||
|
||||
impl Deref for RenderFactory {
|
||||
type Target = ID2D1Factory;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static RENDER_FACTORY: LazyLock<ID2D1Factory> = unsafe {
|
||||
static RENDER_FACTORY: LazyLock<RenderFactory> = unsafe {
|
||||
LazyLock::new(|| {
|
||||
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
|
||||
.expect("creating RENDER_FACTORY failed")
|
||||
RenderFactory(
|
||||
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
|
||||
.expect("creating RENDER_FACTORY failed"),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -100,7 +114,7 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Border {
|
||||
pub hwnd: isize,
|
||||
pub render_target: OnceLock<ID2D1HwndRenderTarget>,
|
||||
pub render_target: OnceLock<RenderTarget>,
|
||||
pub tracking_hwnd: isize,
|
||||
pub window_rect: Rect,
|
||||
pub window_kind: WindowKind,
|
||||
@@ -180,7 +194,7 @@ impl Border {
|
||||
|
||||
loop {
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
tracing::debug!("border window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
@@ -261,7 +275,11 @@ impl Border {
|
||||
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
|
||||
if border.render_target.set(render_target.clone()).is_err() {
|
||||
if border
|
||||
.render_target
|
||||
.set(RenderTarget(render_target.clone()))
|
||||
.is_err()
|
||||
{
|
||||
return Err(anyhow!("could not store border render target"));
|
||||
}
|
||||
|
||||
@@ -275,7 +293,7 @@ impl Border {
|
||||
};
|
||||
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.insert(border.hwnd, render_target);
|
||||
render_targets.insert(border.hwnd, RenderTarget(render_target));
|
||||
Ok(border.clone())
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
@@ -300,7 +318,7 @@ impl Border {
|
||||
|
||||
// this triggers WM_PAINT in the callback below
|
||||
pub fn invalidate(&self) {
|
||||
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
|
||||
let _ = unsafe { InvalidateRect(Option::from(self.hwnd()), None, false) };
|
||||
}
|
||||
|
||||
pub extern "system" fn callback(
|
||||
@@ -508,7 +526,7 @@ impl Border {
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = ValidateRect(window, None);
|
||||
let _ = ValidateRect(Option::from(window), None);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
|
||||
@@ -23,12 +23,14 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use strum::Display;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
@@ -56,8 +58,19 @@ lazy_static! {
|
||||
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
|
||||
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
|
||||
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
|
||||
static ref RENDER_TARGETS: Mutex<HashMap<isize, ID2D1HwndRenderTarget>> =
|
||||
Mutex::new(HashMap::new());
|
||||
static ref RENDER_TARGETS: Mutex<HashMap<isize, RenderTarget>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderTarget(pub ID2D1HwndRenderTarget);
|
||||
unsafe impl Send for RenderTarget {}
|
||||
|
||||
impl Deref for RenderTarget {
|
||||
type Target = ID2D1HwndRenderTarget;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Notification(pub Option<isize>);
|
||||
@@ -211,6 +224,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
|
||||
for window in ws.floating_windows() {
|
||||
let mut window_kind = WindowKind::Unfocused;
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
window_kind = WindowKind::Floating;
|
||||
}
|
||||
|
||||
window.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,6 +274,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.map(|b| b.window_kind == WindowKind::Floating)
|
||||
.unwrap_or_default())
|
||||
});
|
||||
|
||||
if !should_process_notification && switch_focus_to_from_floating_window {
|
||||
should_process_notification = true;
|
||||
}
|
||||
@@ -488,7 +512,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
if new_border {
|
||||
if new_border || should_invalidate {
|
||||
border.set_position(&rect, reference_hwnd)?;
|
||||
}
|
||||
|
||||
@@ -568,7 +592,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum ZOrder {
|
||||
Top,
|
||||
NoTopMost,
|
||||
|
||||
@@ -8,7 +8,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Colour {
|
||||
/// Colour represented as RGB
|
||||
@@ -51,7 +51,7 @@ impl From<Colour> for Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
impl JsonSchema for Hex {
|
||||
@@ -78,7 +78,7 @@ impl From<Colour> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
@@ -120,8 +120,8 @@ impl From<u32> for Rgb {
|
||||
fn from(value: u32) -> Self {
|
||||
Self {
|
||||
r: value & 0xff,
|
||||
g: value >> 8 & 0xff,
|
||||
b: value >> 16 & 0xff,
|
||||
g: (value >> 8) & 0xff,
|
||||
b: (value >> 16) & 0xff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ use windows::core::HRESULT;
|
||||
use windows::core::HSTRING;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::SIZE;
|
||||
use windows::Win32::UI::Shell::Common::IObjectArray;
|
||||
use windows_core::BOOL;
|
||||
|
||||
type DesktopID = GUID;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use windows::Win32::System::Com::CoCreateInstance;
|
||||
use windows::Win32::System::Com::CoInitializeEx;
|
||||
use windows::Win32::System::Com::CoUninitialize;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_APARTMENTTHREADED;
|
||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||
use windows_core::Interface;
|
||||
|
||||
struct ComInit();
|
||||
@@ -23,10 +23,7 @@ struct ComInit();
|
||||
impl ComInit {
|
||||
pub fn new() -> Self {
|
||||
unsafe {
|
||||
// Notice: Only COINIT_APARTMENTTHREADED works correctly!
|
||||
//
|
||||
// Not COINIT_MULTITHREADED or CoIncrementMTAUsage, they cause a seldom crashes in threading tests.
|
||||
CoInitializeEx(None, COINIT_APARTMENTTHREADED).unwrap();
|
||||
CoInitializeEx(None, COINIT_MULTITHREADED).unwrap();
|
||||
}
|
||||
Self()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,16 @@ use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum AnimationStyle {
|
||||
Linear,
|
||||
|
||||
@@ -75,7 +75,7 @@ pub struct IdWithIdentifier {
|
||||
pub matching_strategy: Option<MatchingStrategy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, JsonSchema)]
|
||||
pub enum MatchingStrategy {
|
||||
Legacy,
|
||||
Equals,
|
||||
|
||||
@@ -89,7 +89,6 @@ impl DefaultLayout {
|
||||
return None;
|
||||
};
|
||||
|
||||
let max_divisor = 1.005;
|
||||
let mut r = resize.unwrap_or_default();
|
||||
|
||||
let resize_delta = delta;
|
||||
@@ -108,15 +107,13 @@ impl DefaultLayout {
|
||||
// against this; if people end up in this situation they are better off
|
||||
// just hitting the retile command
|
||||
let diff = ((r.left + -resize_delta) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.right as f32 {
|
||||
r.left += -resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.left - -resize_delta) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.right as f32 {
|
||||
r.left -= -resize_delta;
|
||||
}
|
||||
}
|
||||
@@ -124,15 +121,13 @@ impl DefaultLayout {
|
||||
OperationDirection::Up => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.top + resize_delta) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.top += -resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.top - resize_delta) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.top -= -resize_delta;
|
||||
}
|
||||
}
|
||||
@@ -140,15 +135,13 @@ impl DefaultLayout {
|
||||
OperationDirection::Right => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.right + resize_delta) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.right as f32 {
|
||||
r.right += resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.right - resize_delta) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.right as f32 {
|
||||
r.right -= resize_delta;
|
||||
}
|
||||
}
|
||||
@@ -156,15 +149,13 @@ impl DefaultLayout {
|
||||
OperationDirection::Down => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.bottom + resize_delta) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.bottom += resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.bottom - resize_delta) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.bottom -= resize_delta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,17 @@ use crate::KomorebiTheme;
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
pub use custom_layout::Column;
|
||||
pub use custom_layout::ColumnSplit;
|
||||
pub use custom_layout::ColumnSplitWithCapacity;
|
||||
pub use custom_layout::ColumnWidth;
|
||||
pub use custom_layout::CustomLayout;
|
||||
pub use cycle_direction::CycleDirection;
|
||||
pub use default_layout::DefaultLayout;
|
||||
pub use direction::Direction;
|
||||
pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use pathext::PathExt;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
@@ -37,6 +42,7 @@ pub mod default_layout;
|
||||
pub mod direction;
|
||||
pub mod layout;
|
||||
pub mod operation_direction;
|
||||
pub mod pathext;
|
||||
pub mod rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, JsonSchema)]
|
||||
@@ -117,6 +123,7 @@ pub enum SocketMessage {
|
||||
CycleFocusMonitor(CycleDirection),
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusMonitorAtCursor,
|
||||
FocusLastWorkspace,
|
||||
CloseWorkspace,
|
||||
FocusWorkspaceNumber(usize),
|
||||
@@ -142,6 +149,7 @@ pub enum SocketMessage {
|
||||
NamedWorkspaceLayoutCustomRule(String, usize, PathBuf),
|
||||
ClearWorkspaceLayoutRules(usize, usize),
|
||||
ClearNamedWorkspaceLayoutRules(String),
|
||||
ToggleWorkspaceLayer,
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
ReplaceConfiguration(PathBuf),
|
||||
@@ -178,6 +186,7 @@ pub enum SocketMessage {
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
ResizeDelta(i32),
|
||||
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
|
||||
@@ -330,6 +339,7 @@ pub enum StateQuery {
|
||||
FocusedWorkspaceIndex,
|
||||
FocusedContainerIndex,
|
||||
FocusedWindowIndex,
|
||||
FocusedWorkspaceName,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -428,7 +438,16 @@ pub enum MoveBehaviour {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum CrossBoundaryBehaviour {
|
||||
/// Attempt to perform actions across a workspace boundary
|
||||
@@ -438,7 +457,16 @@ pub enum CrossBoundaryBehaviour {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum HidingBehaviour {
|
||||
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
|
||||
48
komorebi/src/core/pathext.rs
Normal file
48
komorebi/src/core/pathext.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::env;
|
||||
use std::path::Component;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub trait PathExt {
|
||||
fn replace_env(&self) -> PathBuf;
|
||||
}
|
||||
|
||||
impl PathExt for PathBuf {
|
||||
fn replace_env(&self) -> PathBuf {
|
||||
let mut result = PathBuf::new();
|
||||
|
||||
for component in self.components() {
|
||||
match component {
|
||||
Component::Normal(segment) => {
|
||||
// Check if it starts with `$` or `$Env:`
|
||||
if let Some(stripped_segment) = segment.to_string_lossy().strip_prefix('$') {
|
||||
let var_name = if let Some(env_name) = stripped_segment.strip_prefix("Env:")
|
||||
{
|
||||
// Extract the variable name after `$Env:`
|
||||
env_name
|
||||
} else if stripped_segment == "HOME" {
|
||||
// Special case for `$HOME`
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
// Extract the variable name after `$`
|
||||
stripped_segment
|
||||
};
|
||||
|
||||
if let Ok(value) = env::var(var_name) {
|
||||
result.push(&value); // Replace with the value
|
||||
} else {
|
||||
result.push(segment); // Keep as-is if variable is not found
|
||||
}
|
||||
} else {
|
||||
result.push(segment); // Keep as-is if not an environment variable
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Add other components (e.g., root, parent) as-is
|
||||
result.push(component.as_os_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ pub mod workspace;
|
||||
pub mod workspace_reconciliator;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use monitor_reconciliator::MonitorNotification;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
@@ -127,6 +128,7 @@ lazy_static! {
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST: Arc<Mutex<Vec<Regex>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref TRANSPARENCY_BLACKLIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
@@ -180,7 +182,7 @@ lazy_static! {
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
|
||||
Arc::new(Mutex::new(HidingBehaviour::Minimize));
|
||||
Arc::new(Mutex::new(HidingBehaviour::Cloak));
|
||||
pub static ref HOME_DIR: PathBuf = {
|
||||
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
@@ -219,6 +221,8 @@ lazy_static! {
|
||||
|
||||
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
@@ -281,6 +285,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
Monitor(MonitorNotification),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -176,7 +176,7 @@ fn main() -> Result<()> {
|
||||
let session_id = WindowsApi::process_id_to_session_id()?;
|
||||
SESSION_ID.store(session_id, Ordering::SeqCst);
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_processes(ProcessesToUpdate::All, true);
|
||||
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
|
||||
@@ -268,8 +268,14 @@ 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);
|
||||
if let Ok(state) = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?) {
|
||||
wm.lock().apply_state(state);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"cannot apply state from {}; state struct is not up to date",
|
||||
dumped_state.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wm.lock().retile_all(false)?;
|
||||
@@ -285,7 +291,7 @@ fn main() -> Result<()> {
|
||||
transparency_manager::listen_for_notifications(wm.clone());
|
||||
workspace_reconciliator::listen_for_notifications(wm.clone());
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::watch_for_orphans(wm.clone());
|
||||
reaper::listen_for_notifications(wm.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
theme_manager::listen_for_notifications();
|
||||
|
||||
|
||||
@@ -36,33 +36,58 @@ use crate::WindowsApi;
|
||||
)]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
id: isize,
|
||||
pub id: isize,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
name: String,
|
||||
pub name: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
device: String,
|
||||
pub device: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
device_id: String,
|
||||
pub device_id: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
size: Rect,
|
||||
pub serial_number_id: Option<String>,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
work_area_size: Rect,
|
||||
pub size: Rect,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub work_area_size: Rect,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
work_area_offset: Option<Rect>,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
window_based_work_area_offset: Option<Rect>,
|
||||
pub window_based_work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
window_based_work_area_offset_limit: isize,
|
||||
workspaces: Ring<Workspace>,
|
||||
pub window_based_work_area_offset_limit: isize,
|
||||
pub workspaces: Ring<Workspace>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
last_focused_workspace: Option<usize>,
|
||||
pub last_focused_workspace: Option<usize>,
|
||||
#[getset(get_mut = "pub")]
|
||||
workspace_names: HashMap<usize, String>,
|
||||
pub workspace_names: HashMap<usize, String>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Monitor, Workspace);
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MonitorInformation {
|
||||
pub id: isize,
|
||||
pub name: String,
|
||||
pub device: String,
|
||||
pub device_id: String,
|
||||
pub serial_number_id: Option<String>,
|
||||
pub size: Rect,
|
||||
}
|
||||
|
||||
impl From<&Monitor> for MonitorInformation {
|
||||
fn from(monitor: &Monitor) -> Self {
|
||||
Self {
|
||||
id: monitor.id,
|
||||
name: monitor.name.clone(),
|
||||
device: monitor.device.clone(),
|
||||
device_id: monitor.device_id.clone(),
|
||||
serial_number_id: monitor.serial_number_id.clone(),
|
||||
size: monitor.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
id: isize,
|
||||
size: Rect,
|
||||
@@ -70,6 +95,7 @@ pub fn new(
|
||||
name: String,
|
||||
device: String,
|
||||
device_id: String,
|
||||
serial_number_id: Option<String>,
|
||||
) -> Monitor {
|
||||
let mut workspaces = Ring::default();
|
||||
workspaces.elements_mut().push_back(Workspace::default());
|
||||
@@ -79,6 +105,7 @@ pub fn new(
|
||||
name,
|
||||
device,
|
||||
device_id,
|
||||
serial_number_id,
|
||||
size,
|
||||
work_area_size,
|
||||
work_area_offset: None,
|
||||
@@ -91,12 +118,33 @@ pub fn new(
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
pub fn new(
|
||||
id: isize,
|
||||
size: Rect,
|
||||
work_area_size: Rect,
|
||||
name: String,
|
||||
device: String,
|
||||
device_id: String,
|
||||
serial_number_id: Option<String>,
|
||||
) -> Self {
|
||||
new(
|
||||
id,
|
||||
size,
|
||||
work_area_size,
|
||||
name,
|
||||
device,
|
||||
device_id,
|
||||
serial_number_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn placeholder() -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
name: "PLACEHOLDER".to_string(),
|
||||
device: "".to_string(),
|
||||
device_id: "".to_string(),
|
||||
serial_number_id: None,
|
||||
size: Default::default(),
|
||||
work_area_size: Default::default(),
|
||||
work_area_offset: None,
|
||||
@@ -107,6 +155,13 @@ impl Monitor {
|
||||
workspace_names: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_workspace_name(&self) -> Option<String> {
|
||||
self.focused_workspace()
|
||||
.map(|w| w.name().clone())
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
let focused_idx = self.focused_workspace_idx();
|
||||
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
||||
|
||||
@@ -2,21 +2,33 @@ use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::System::Power::POWERBROADCAST_SETTING;
|
||||
use windows::Win32::System::SystemServices::GUID_LIDSWITCH_STATE_CHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_CONFIGCHANGED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEARRIVAL;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEREMOVECOMPLETE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVTYP_DEVICEINTERFACE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_POWERSETTINGCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
|
||||
@@ -75,7 +87,7 @@ impl Hidden {
|
||||
|
||||
loop {
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
tracing::debug!("hidden window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
@@ -92,8 +104,57 @@ impl Hidden {
|
||||
|
||||
let hwnd = hwnd_receiver.recv()?;
|
||||
|
||||
// Register Session Lock/Unlock events
|
||||
WindowsApi::wts_register_session_notification(hwnd)?;
|
||||
|
||||
// Register Laptop lid open/close events
|
||||
WindowsApi::register_power_setting_notification(
|
||||
hwnd,
|
||||
&GUID_LIDSWITCH_STATE_CHANGE,
|
||||
REGISTER_NOTIFICATION_FLAGS(0),
|
||||
)?;
|
||||
|
||||
// Register device interface events for multiple display related devices. Some of this
|
||||
// device interfaces might not be needed but it doesn't hurt to have them in case some user
|
||||
// uses some output device as monitor that falls into one of these device interface class
|
||||
// GUID.
|
||||
let monitor_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
|
||||
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
|
||||
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
|
||||
dbcc_reserved: 0,
|
||||
dbcc_classguid: GUID_DEVINTERFACE_MONITOR,
|
||||
dbcc_name: [0; 1],
|
||||
};
|
||||
let display_adapter_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
|
||||
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
|
||||
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
|
||||
dbcc_reserved: 0,
|
||||
dbcc_classguid: GUID_DEVINTERFACE_DISPLAY_ADAPTER,
|
||||
dbcc_name: [0; 1],
|
||||
};
|
||||
let video_output_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
|
||||
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
|
||||
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
|
||||
dbcc_reserved: 0,
|
||||
dbcc_classguid: GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL,
|
||||
dbcc_name: [0; 1],
|
||||
};
|
||||
WindowsApi::register_device_notification(
|
||||
hwnd,
|
||||
monitor_filter,
|
||||
REGISTER_NOTIFICATION_FLAGS(0),
|
||||
)?;
|
||||
WindowsApi::register_device_notification(
|
||||
hwnd,
|
||||
display_adapter_filter,
|
||||
REGISTER_NOTIFICATION_FLAGS(0),
|
||||
)?;
|
||||
WindowsApi::register_device_notification(
|
||||
hwnd,
|
||||
video_output_filter,
|
||||
REGISTER_NOTIFICATION_FLAGS(0),
|
||||
)?;
|
||||
|
||||
Ok(Self { hwnd })
|
||||
}
|
||||
|
||||
@@ -114,7 +175,7 @@ impl Hidden {
|
||||
"WM_POWERBROADCAST event received - resume from suspend"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::ResumingFromSuspendedState,
|
||||
monitor_reconciliator::MonitorNotification::ResumingFromSuspendedState,
|
||||
);
|
||||
LRESULT(0)
|
||||
}
|
||||
@@ -124,10 +185,39 @@ impl Hidden {
|
||||
"WM_POWERBROADCAST event received - entering suspended state"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::EnteringSuspendedState,
|
||||
monitor_reconciliator::MonitorNotification::EnteringSuspendedState,
|
||||
);
|
||||
LRESULT(0)
|
||||
}
|
||||
// Monitor change power status
|
||||
PBT_POWERSETTINGCHANGE => {
|
||||
if let POWERBROADCAST_SETTING {
|
||||
PowerSetting: GUID_LIDSWITCH_STATE_CHANGE,
|
||||
DataLength: _,
|
||||
Data: [0],
|
||||
} = *(lparam.0 as *const POWERBROADCAST_SETTING)
|
||||
{
|
||||
tracing::debug!(
|
||||
"WM_POWERBROADCAST event received - laptop lid closed"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
|
||||
);
|
||||
} else if let POWERBROADCAST_SETTING {
|
||||
PowerSetting: GUID_LIDSWITCH_STATE_CHANGE,
|
||||
DataLength: _,
|
||||
Data: [1],
|
||||
} = *(lparam.0 as *const POWERBROADCAST_SETTING)
|
||||
{
|
||||
tracing::debug!(
|
||||
"WM_POWERBROADCAST event received - laptop lid opened"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
|
||||
);
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => LRESULT(0),
|
||||
}
|
||||
}
|
||||
@@ -137,14 +227,14 @@ impl Hidden {
|
||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::SessionLocked,
|
||||
monitor_reconciliator::MonitorNotification::SessionLocked,
|
||||
);
|
||||
}
|
||||
WTS_SESSION_UNLOCK => {
|
||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::SessionUnlocked,
|
||||
monitor_reconciliator::MonitorNotification::SessionUnlocked,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
@@ -165,7 +255,7 @@ impl Hidden {
|
||||
);
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::ResolutionScalingChanged,
|
||||
monitor_reconciliator::MonitorNotification::ResolutionScalingChanged,
|
||||
);
|
||||
LRESULT(0)
|
||||
}
|
||||
@@ -179,7 +269,7 @@ impl Hidden {
|
||||
);
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::WorkAreaChanged,
|
||||
monitor_reconciliator::MonitorNotification::WorkAreaChanged,
|
||||
);
|
||||
}
|
||||
LRESULT(0)
|
||||
@@ -188,12 +278,17 @@ impl Hidden {
|
||||
// Original idea from https://stackoverflow.com/a/33762334
|
||||
WM_DEVICECHANGE => {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
|
||||
let event = wparam.0 as u32;
|
||||
if event == DBT_DEVNODES_CHANGED
|
||||
|| event == DBT_CONFIGCHANGED
|
||||
|| event == DBT_DEVICEARRIVAL
|
||||
|| event == DBT_DEVICEREMOVECOMPLETE
|
||||
{
|
||||
tracing::debug!(
|
||||
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
|
||||
);
|
||||
"WM_DEVICECHANGE event received with one of [DBT_DEVNODES_CHANGED, DBT_CONFIGCHANGED, DBT_DEVICEARRIVAL, DBT_DEVICEREMOVECOMPLETE] - display added or removed"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::DisplayConnectionChange,
|
||||
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::core::Rect;
|
||||
use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator::hidden::Hidden;
|
||||
use crate::MonitorConfig;
|
||||
use crate::notify_subscribers;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -20,7 +28,9 @@ use std::sync::OnceLock;
|
||||
|
||||
pub mod hidden;
|
||||
|
||||
pub enum Notification {
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum MonitorNotification {
|
||||
ResolutionScalingChanged,
|
||||
WorkAreaChanged,
|
||||
DisplayConnectionChange,
|
||||
@@ -32,34 +42,35 @@ pub enum Notification {
|
||||
|
||||
static ACTIVE: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
|
||||
OnceLock::new();
|
||||
|
||||
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = OnceLock::new();
|
||||
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, Monitor>>> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
|
||||
pub fn channel() -> &'static (Sender<MonitorNotification>, Receiver<MonitorNotification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
fn event_tx() -> Sender<MonitorNotification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
fn event_rx() -> Receiver<MonitorNotification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification(notification: Notification) {
|
||||
pub fn send_notification(notification: MonitorNotification) {
|
||||
if event_tx().try_send(notification).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
|
||||
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
||||
let mut monitor_cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
|
||||
monitor_cache.insert(device_id.to_string(), config);
|
||||
monitor_cache.insert(serial_or_device_id.to_string(), monitor);
|
||||
}
|
||||
|
||||
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
|
||||
@@ -89,10 +100,12 @@ pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
|
||||
name,
|
||||
device,
|
||||
device_id,
|
||||
display.serial_number_id,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
#[allow(clippy::expect_used)]
|
||||
Hidden::create("komorebi-hidden")?;
|
||||
@@ -116,41 +129,40 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
'receiver: for notification in receiver {
|
||||
if !ACTIVE.load_consume() {
|
||||
if matches!(
|
||||
if !ACTIVE.load_consume()
|
||||
&& matches!(
|
||||
notification,
|
||||
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked
|
||||
) {
|
||||
tracing::debug!(
|
||||
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
|
||||
);
|
||||
MonitorNotification::ResumingFromSuspendedState
|
||||
| MonitorNotification::SessionUnlocked
|
||||
)
|
||||
{
|
||||
tracing::debug!(
|
||||
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
|
||||
);
|
||||
|
||||
ACTIVE.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
ACTIVE.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
let mut wm = wm.lock();
|
||||
|
||||
let initial_state = State::from(wm.as_ref());
|
||||
|
||||
match notification {
|
||||
Notification::EnteringSuspendedState | Notification::SessionLocked => {
|
||||
MonitorNotification::EnteringSuspendedState | MonitorNotification::SessionLocked => {
|
||||
tracing::debug!(
|
||||
"deactivating reconciliator until system resumes from suspended state or session is unlocked"
|
||||
);
|
||||
ACTIVE.store(false, Ordering::SeqCst);
|
||||
}
|
||||
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => {
|
||||
// this is only handled above if the reconciliator is paused
|
||||
}
|
||||
Notification::WorkAreaChanged => {
|
||||
MonitorNotification::WorkAreaChanged => {
|
||||
tracing::debug!("handling work area changed notification");
|
||||
let offset = wm.work_area_offset;
|
||||
for monitor in wm.monitors_mut() {
|
||||
@@ -182,7 +194,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
}
|
||||
}
|
||||
Notification::ResolutionScalingChanged => {
|
||||
MonitorNotification::ResolutionScalingChanged => {
|
||||
tracing::debug!("handling resolution/scaling changed notification");
|
||||
let offset = wm.work_area_offset;
|
||||
for monitor in wm.monitors_mut() {
|
||||
@@ -229,7 +241,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
}
|
||||
}
|
||||
Notification::DisplayConnectionChange => {
|
||||
// this is handled above if the reconciliator is paused but we should still check if
|
||||
// there were any changes to the connected monitors while the system was
|
||||
// suspended/locked.
|
||||
MonitorNotification::ResumingFromSuspendedState
|
||||
| MonitorNotification::SessionUnlocked
|
||||
| MonitorNotification::DisplayConnectionChange => {
|
||||
tracing::debug!("handling display connection change notification");
|
||||
let mut monitor_cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
@@ -243,8 +260,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Make sure that in our state any attached displays have the latest Win32 data
|
||||
for monitor in wm.monitors_mut() {
|
||||
for attached in &attached_devices {
|
||||
if attached.device_id().eq(monitor.device_id()) {
|
||||
if attached.serial_number_id().eq(monitor.serial_number_id())
|
||||
|| attached.device_id().eq(monitor.device_id())
|
||||
{
|
||||
monitor.set_id(attached.id());
|
||||
monitor.set_device_id(attached.device_id().clone());
|
||||
monitor.set_serial_number_id(attached.serial_number_id().clone());
|
||||
monitor.set_name(attached.name().clone());
|
||||
monitor.set_size(*attached.size());
|
||||
monitor.set_work_area_size(*attached.work_area_size());
|
||||
@@ -254,6 +275,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
if initial_monitor_count == attached_devices.len() {
|
||||
tracing::debug!("monitor counts match, reconciliation not required");
|
||||
drop(wm);
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -261,6 +283,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
tracing::debug!(
|
||||
"no devices found, skipping reconciliation to avoid breaking state"
|
||||
);
|
||||
drop(wm);
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -270,41 +293,108 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
attached_devices.len()
|
||||
);
|
||||
|
||||
// Gather all the containers that will be orphaned from disconnected and invalid displays
|
||||
let mut orphaned_containers = vec![];
|
||||
// Windows to remove from `known_hwnds`
|
||||
let mut windows_to_remove = Vec::new();
|
||||
|
||||
// Collect the ids in our state which aren't in the current attached display ids
|
||||
// These are monitors that have been removed
|
||||
let mut newly_removed_displays = vec![];
|
||||
|
||||
for m in wm.monitors().iter() {
|
||||
if !attached_devices
|
||||
.iter()
|
||||
.any(|attached| attached.device_id().eq(m.device_id()))
|
||||
{
|
||||
newly_removed_displays.push(m.device_id().clone());
|
||||
for workspace in m.workspaces() {
|
||||
for container in workspace.containers() {
|
||||
// Save the orphaned containers from the removed monitor
|
||||
orphaned_containers.push(container.clone());
|
||||
for (m_idx, m) in wm.monitors().iter().enumerate() {
|
||||
if !attached_devices.iter().any(|attached| {
|
||||
attached.serial_number_id().eq(m.serial_number_id())
|
||||
|| attached.device_id().eq(m.device_id())
|
||||
}) {
|
||||
let id = m
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.map_or(m.device_id().clone(), |sn| sn.clone());
|
||||
|
||||
newly_removed_displays.push(id.clone());
|
||||
|
||||
let focused_workspace_idx = m.focused_workspace_idx();
|
||||
|
||||
for (idx, workspace) in m.workspaces().iter().enumerate() {
|
||||
let is_focused_workspace = idx == focused_workspace_idx;
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
for (c_idx, container) in workspace.containers().iter().enumerate()
|
||||
{
|
||||
let focused_window_idx = container.focused_window_idx();
|
||||
for (w_idx, window) in container.windows().iter().enumerate() {
|
||||
windows_to_remove.push(window.hwnd);
|
||||
if is_focused_workspace
|
||||
&& c_idx == focused_container_idx
|
||||
&& w_idx == focused_window_idx
|
||||
{
|
||||
// Minimize the focused window since Windows might try
|
||||
// to move it to another monitor if it was focused.
|
||||
if window.is_focused() {
|
||||
window.minimize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(maximized) = workspace.maximized_window() {
|
||||
windows_to_remove.push(maximized.hwnd);
|
||||
// Minimize the focused window since Windows might try
|
||||
// to move it to another monitor if it was focused.
|
||||
if maximized.is_focused() {
|
||||
maximized.minimize();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
for window in container.windows() {
|
||||
windows_to_remove.push(window.hwnd);
|
||||
}
|
||||
if let Some(window) = container.focused_window() {
|
||||
// Minimize the focused window since Windows might try
|
||||
// to move it to another monitor if it was focused.
|
||||
if window.is_focused() {
|
||||
window.minimize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
windows_to_remove.push(window.hwnd);
|
||||
// Minimize the focused window since Windows might try
|
||||
// to move it to another monitor if it was focused.
|
||||
if window.is_focused() {
|
||||
window.minimize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any workspace_rules for this specific monitor
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
let mut rules_to_remove = Vec::new();
|
||||
for (i, rule) in workspace_rules.iter().enumerate().rev() {
|
||||
if rule.monitor_index == m_idx {
|
||||
rules_to_remove.push(i);
|
||||
}
|
||||
}
|
||||
for i in rules_to_remove {
|
||||
workspace_rules.remove(i);
|
||||
}
|
||||
|
||||
// Let's add their state to the cache for later
|
||||
monitor_cache.insert(m.device_id().clone(), m.into());
|
||||
monitor_cache.insert(id, m.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !orphaned_containers.is_empty() {
|
||||
tracing::info!(
|
||||
"removed orphaned containers from: {newly_removed_displays:?}"
|
||||
);
|
||||
}
|
||||
// Update known_hwnds
|
||||
wm.known_hwnds.retain(|i, _| !windows_to_remove.contains(i));
|
||||
|
||||
if !newly_removed_displays.is_empty() {
|
||||
// After we have cached them, remove them from our state
|
||||
wm.monitors_mut()
|
||||
.retain(|m| !newly_removed_displays.contains(m.device_id()));
|
||||
wm.monitors_mut().retain(|m| {
|
||||
!newly_removed_displays.iter().any(|id| {
|
||||
m.serial_number_id().as_ref().is_some_and(|sn| sn == id)
|
||||
|| m.device_id() == id
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let post_removal_monitor_count = wm.monitors().len();
|
||||
@@ -313,24 +403,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
wm.focus_monitor(0)?;
|
||||
}
|
||||
|
||||
if !orphaned_containers.is_empty() {
|
||||
if let Some(primary) = wm.monitors_mut().front_mut() {
|
||||
if let Some(focused_ws) = primary.focused_workspace_mut() {
|
||||
let focused_container_idx = focused_ws.focused_container_idx();
|
||||
|
||||
// Put the orphaned containers somewhere visible
|
||||
for container in orphaned_containers {
|
||||
focused_ws.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Gotta reset the focus or the movement will feel "off"
|
||||
if initial_monitor_count != post_removal_monitor_count {
|
||||
focused_ws.focus_container(focused_container_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let offset = wm.work_area_offset;
|
||||
|
||||
for monitor in wm.monitors_mut() {
|
||||
@@ -343,7 +415,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
let post_removal_monitor_count = wm.monitors().len();
|
||||
|
||||
// This is the list of device ids after we have removed detached displays
|
||||
// This is the list of device ids after we have removed detached displays. We can
|
||||
// keep this with just the device_ids without the serial numbers since this is used
|
||||
// only to check which one is the newly added monitor below if there is a new
|
||||
// monitor. Everything done after with said new monitor will again consider both
|
||||
// serial number and device ids.
|
||||
let post_removal_device_ids = wm
|
||||
.monitors()
|
||||
.iter()
|
||||
@@ -353,7 +429,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
// Check for and add any new monitors that may have been plugged in
|
||||
// Monitor and display index preferences get applied in this function
|
||||
WindowsApi::load_monitor_information(&mut wm.monitors)?;
|
||||
WindowsApi::load_monitor_information(&mut wm)?;
|
||||
|
||||
let post_addition_monitor_count = wm.monitors().len();
|
||||
|
||||
@@ -362,42 +438,181 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
"monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors",
|
||||
);
|
||||
|
||||
let known_hwnds = wm.known_hwnds.clone();
|
||||
let offset = wm.work_area_offset;
|
||||
let mouse_follows_focus = wm.mouse_follows_focus;
|
||||
let focused_monitor_idx = wm.focused_monitor_idx();
|
||||
let focused_workspace_idx = wm.focused_workspace_idx()?;
|
||||
|
||||
// Look in the updated state for new monitors
|
||||
for m in wm.monitors_mut() {
|
||||
let device_id = m.device_id().clone();
|
||||
for (i, m) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let device_id = m.device_id();
|
||||
// We identify a new monitor when we encounter a new device id
|
||||
if !post_removal_device_ids.contains(&device_id) {
|
||||
if !post_removal_device_ids.contains(device_id) {
|
||||
let mut cache_hit = false;
|
||||
let mut cached_id = String::new();
|
||||
// Check if that device id exists in the cache for this session
|
||||
if let Some(cached) = monitor_cache.get(&device_id) {
|
||||
if let Some((id, cached)) = monitor_cache.get_key_value(device_id).or(m
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.and_then(|sn| monitor_cache.get_key_value(sn)))
|
||||
{
|
||||
cache_hit = true;
|
||||
cached_id = id.clone();
|
||||
|
||||
tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying");
|
||||
tracing::info!("found monitor and workspace configuration for {id} in the monitor cache, applying");
|
||||
|
||||
// If it does, load all the monitor settings from the cache entry
|
||||
m.ensure_workspace_count(cached.workspaces.len());
|
||||
m.set_work_area_offset(cached.work_area_offset);
|
||||
m.set_window_based_work_area_offset(
|
||||
cached.window_based_work_area_offset,
|
||||
);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
cached.window_based_work_area_offset_limit.unwrap_or(1),
|
||||
);
|
||||
// If it does, load the monitor removing any window that has since
|
||||
// been closed or moved to another workspace
|
||||
*m = cached.clone();
|
||||
|
||||
for (w_idx, workspace) in m.workspaces_mut().iter_mut().enumerate()
|
||||
{
|
||||
if let Some(cached_workspace) = cached.workspaces.get(w_idx) {
|
||||
workspace.load_static_config(cached_workspace)?;
|
||||
let focused_workspace_idx = m.focused_workspace_idx();
|
||||
|
||||
for (j, workspace) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
// If this is the focused workspace we need to show (restore) all
|
||||
// windows that were visible since they were probably minimized by
|
||||
// Windows.
|
||||
let is_focused_workspace = j == focused_workspace_idx;
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
|
||||
let mut empty_containers = Vec::new();
|
||||
for (idx, container) in
|
||||
workspace.containers_mut().iter_mut().enumerate()
|
||||
{
|
||||
container.windows_mut().retain(|window| {
|
||||
window.exe().is_ok()
|
||||
&& !known_hwnds.contains_key(&window.hwnd)
|
||||
});
|
||||
|
||||
if container.windows().is_empty() {
|
||||
empty_containers.push(idx);
|
||||
}
|
||||
|
||||
if is_focused_workspace {
|
||||
if let Some(window) = container.focused_window() {
|
||||
tracing::debug!(
|
||||
"restoring window: {}",
|
||||
window.hwnd
|
||||
);
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
} else {
|
||||
// If the focused window was moved or removed by
|
||||
// the user after the disconnect then focus the
|
||||
// first window and show that one
|
||||
container.focus_window(0);
|
||||
|
||||
if let Some(window) = container.focused_window() {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty containers
|
||||
for empty_idx in empty_containers {
|
||||
if empty_idx == focused_container_idx {
|
||||
workspace.remove_container(empty_idx);
|
||||
} else {
|
||||
workspace.remove_container_by_idx(empty_idx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = workspace.maximized_window() {
|
||||
if window.exe().is_err()
|
||||
|| known_hwnds.contains_key(&window.hwnd)
|
||||
{
|
||||
workspace.set_maximized_window(None);
|
||||
} else if is_focused_workspace {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container_mut() {
|
||||
container.windows_mut().retain(|window| {
|
||||
window.exe().is_ok()
|
||||
&& !known_hwnds.contains_key(&window.hwnd)
|
||||
});
|
||||
|
||||
if container.windows().is_empty() {
|
||||
workspace.set_monocle_container(None);
|
||||
} else if is_focused_workspace {
|
||||
if let Some(window) = container.focused_window() {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
} else {
|
||||
// If the focused window was moved or removed by
|
||||
// the user after the disconnect then focus the
|
||||
// first window and show that one
|
||||
container.focus_window(0);
|
||||
|
||||
if let Some(window) = container.focused_window() {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace.floating_windows_mut().retain(|window| {
|
||||
window.exe().is_ok()
|
||||
&& !known_hwnds.contains_key(&window.hwnd)
|
||||
});
|
||||
|
||||
if is_focused_workspace {
|
||||
for window in workspace.floating_windows() {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply workspace rules
|
||||
let mut workspace_matching_rules =
|
||||
WORKSPACE_MATCHING_RULES.lock();
|
||||
if let Some(rules) = workspace
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.workspace_rules.as_ref())
|
||||
{
|
||||
for r in rules {
|
||||
workspace_matching_rules.push(WorkspaceMatchingRule {
|
||||
monitor_index: i,
|
||||
workspace_index: j,
|
||||
matching_rule: r.clone(),
|
||||
initial_only: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rules) = workspace
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.initial_workspace_rules.as_ref())
|
||||
{
|
||||
for r in rules {
|
||||
workspace_matching_rules.push(WorkspaceMatchingRule {
|
||||
monitor_index: i,
|
||||
workspace_index: j,
|
||||
matching_rule: r.clone(),
|
||||
initial_only: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore windows from new monitor and update the focused
|
||||
// workspace
|
||||
m.load_focused_workspace(mouse_follows_focus)?;
|
||||
m.update_focused_workspace(offset)?;
|
||||
}
|
||||
|
||||
// Entries in the cache should only be used once; remove the entry there was a cache hit
|
||||
if cache_hit {
|
||||
monitor_cache.remove(&device_id);
|
||||
if cache_hit && !cached_id.is_empty() {
|
||||
monitor_cache.remove(&cached_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refocus the previously focused monitor since the code above might
|
||||
// steal the focus away.
|
||||
wm.focus_monitor(focused_monitor_idx)?;
|
||||
wm.focus_workspace(focused_workspace_idx)?;
|
||||
}
|
||||
|
||||
let final_count = wm.monitors().len();
|
||||
@@ -411,6 +626,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::Monitor(notification),
|
||||
state: wm.as_ref().into(),
|
||||
},
|
||||
initial_state.has_been_modified(&wm),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -52,6 +52,7 @@ use crate::border_manager::STYLE;
|
||||
use crate::colour::Rgb;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::monitor::MonitorInformation;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
@@ -65,6 +66,7 @@ use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::workspace::WorkspaceWindowLocation;
|
||||
use crate::GlobalState;
|
||||
use crate::Notification;
|
||||
@@ -290,13 +292,37 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusWindow(direction) => {
|
||||
self.focus_container_in_direction(direction)?;
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
match focused_workspace.layer() {
|
||||
WorkspaceLayer::Tiling => {
|
||||
self.focus_container_in_direction(direction)?;
|
||||
}
|
||||
WorkspaceLayer::Floating => {
|
||||
self.focus_floating_window_in_direction(direction)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::MoveWindow(direction) => {
|
||||
self.move_container_in_direction(direction)?;
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
match focused_workspace.layer() {
|
||||
WorkspaceLayer::Tiling => {
|
||||
self.move_container_in_direction(direction)?;
|
||||
}
|
||||
WorkspaceLayer::Floating => {
|
||||
self.move_floating_window_in_direction(direction)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::CycleFocusWindow(direction) => {
|
||||
self.focus_container_in_cycle_direction(direction)?;
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
match focused_workspace.layer() {
|
||||
WorkspaceLayer::Tiling => {
|
||||
self.focus_container_in_cycle_direction(direction)?;
|
||||
}
|
||||
WorkspaceLayer::Floating => {
|
||||
self.focus_floating_window_in_cycle_direction(direction)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::CycleMoveWindow(direction) => {
|
||||
self.move_container_in_cycle_direction(direction)?;
|
||||
@@ -734,6 +760,11 @@ impl WindowManager {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
}
|
||||
SocketMessage::FocusMonitorAtCursor => {
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::Retile => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(false)?
|
||||
@@ -847,7 +878,15 @@ impl WindowManager {
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let focused_monitor = self
|
||||
@@ -870,7 +909,15 @@ impl WindowManager {
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut can_close = false;
|
||||
@@ -906,7 +953,15 @@ impl WindowManager {
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let idx = self
|
||||
@@ -929,7 +984,15 @@ impl WindowManager {
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.focused_workspace_idx().unwrap_or_default() != workspace_idx {
|
||||
@@ -941,7 +1004,15 @@ impl WindowManager {
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
@@ -974,6 +1045,29 @@ impl WindowManager {
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleWorkspaceLayer => {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
match workspace.layer() {
|
||||
WorkspaceLayer::Tiling => {
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
|
||||
if let Some(first) = workspace.floating_windows().first() {
|
||||
first.focus(mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
WorkspaceLayer::Floating => {
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
if let Some(container) = workspace.focused_container() {
|
||||
if let Some(window) = container.focused_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
SocketMessage::Stop => {
|
||||
self.stop(false)?;
|
||||
}
|
||||
@@ -1051,9 +1145,9 @@ impl WindowManager {
|
||||
reply.write_all(visible_windows_state.as_bytes())?;
|
||||
}
|
||||
SocketMessage::MonitorInformation => {
|
||||
let mut monitors = HashMap::new();
|
||||
let mut monitors = vec![];
|
||||
for monitor in self.monitors() {
|
||||
monitors.insert(monitor.device_id(), monitor.size());
|
||||
monitors.push(MonitorInformation::from(monitor));
|
||||
}
|
||||
|
||||
let monitors_state = serde_json::to_string_pretty(&monitors)
|
||||
@@ -1063,19 +1157,29 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::Query(query) => {
|
||||
let response = match query {
|
||||
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
|
||||
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx().to_string(),
|
||||
StateQuery::FocusedWorkspaceIndex => self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.focused_workspace_idx(),
|
||||
StateQuery::FocusedContainerIndex => {
|
||||
self.focused_workspace()?.focused_container_idx()
|
||||
}
|
||||
.focused_workspace_idx()
|
||||
.to_string(),
|
||||
StateQuery::FocusedContainerIndex => self
|
||||
.focused_workspace()?
|
||||
.focused_container_idx()
|
||||
.to_string(),
|
||||
StateQuery::FocusedWindowIndex => {
|
||||
self.focused_container()?.focused_window_idx()
|
||||
self.focused_container()?.focused_window_idx().to_string()
|
||||
}
|
||||
}
|
||||
.to_string();
|
||||
StateQuery::FocusedWorkspaceName => {
|
||||
let focused_monitor = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
focused_monitor
|
||||
.focused_workspace_name()
|
||||
.unwrap_or_else(|| focused_monitor.focused_workspace_idx().to_string())
|
||||
}
|
||||
};
|
||||
|
||||
reply.write_all(response.as_bytes())?;
|
||||
}
|
||||
@@ -1305,6 +1409,8 @@ impl WindowManager {
|
||||
// Initialize the new wm
|
||||
wm.init()?;
|
||||
|
||||
wm.restore_all_windows(true)?;
|
||||
|
||||
// This is equivalent to StaticConfig::postload for this use case
|
||||
StaticConfig::reload(config, &mut wm)?;
|
||||
|
||||
@@ -1400,6 +1506,14 @@ impl WindowManager {
|
||||
self.retile_all(false)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.set_apply_window_based_work_area_offset(
|
||||
!workspace.apply_window_based_work_area_offset(),
|
||||
);
|
||||
|
||||
self.retile_all(false)?;
|
||||
}
|
||||
SocketMessage::QuickSave => {
|
||||
let workspace = self.focused_workspace()?;
|
||||
let resize = workspace.resize_dimensions();
|
||||
@@ -1769,6 +1883,9 @@ impl WindowManager {
|
||||
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
|
||||
};
|
||||
|
||||
// Update list of known_hwnds and their monitor/workspace index pair
|
||||
self.update_known_hwnds();
|
||||
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -33,7 +32,6 @@ use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::DATA_DIR;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
@@ -93,8 +91,18 @@ impl WindowManager {
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if w.contains_managed_window(event_hwnd)
|
||||
&& !visible_hwnds.contains(&event_hwnd)
|
||||
let contains_managed_window = w.contains_managed_window(event_hwnd);
|
||||
|
||||
// this is for an old stackbar clicking fix
|
||||
if contains_managed_window && !visible_hwnds.contains(&event_hwnd) {
|
||||
transparency_override = true;
|
||||
}
|
||||
|
||||
// but we always want to handle a minimize event when transparency overrides
|
||||
// are applied
|
||||
if !transparency_override
|
||||
&& contains_managed_window
|
||||
&& matches!(event, WindowManagerEvent::Minimize(_, _))
|
||||
{
|
||||
transparency_override = true;
|
||||
}
|
||||
@@ -244,7 +252,12 @@ impl WindowManager {
|
||||
already_moved_window_handles.remove(&window.hwnd);
|
||||
}
|
||||
WindowManagerEvent::FocusChange(_, window) => {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
// don't want to trigger the full workspace updates when there are no managed
|
||||
// containers - this makes floating windows on empty workspaces go into very
|
||||
// annoying focus change loops which prevents users from interacting with them
|
||||
if !self.focused_workspace()?.containers().is_empty() {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let floating_window_idx = workspace
|
||||
@@ -292,30 +305,28 @@ impl WindowManager {
|
||||
|
||||
let mut needs_reconciliation = false;
|
||||
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitors.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
}
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if focused_pair != (*m_idx, *w_idx) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(i, j);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(*m_idx, *w_idx);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,11 +337,14 @@ impl WindowManager {
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd)
|
||||
&& i != self.focused_monitor_idx()
|
||||
&& j != monitor.focused_workspace_idx()
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if let Some(focused_workspace_idx) = self
|
||||
.monitors()
|
||||
.get(*m_idx)
|
||||
.map(|m| m.focused_workspace_idx())
|
||||
{
|
||||
if *m_idx != self.focused_monitor_idx()
|
||||
&& *w_idx != focused_workspace_idx
|
||||
{
|
||||
tracing::debug!(
|
||||
"ignoring show event for window already associated with another workspace"
|
||||
@@ -489,15 +503,9 @@ impl WindowManager {
|
||||
// This will be true if we have moved to another monitor
|
||||
let mut moved_across_monitors = false;
|
||||
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for workspace in monitors.workspaces() {
|
||||
if workspace.contains_window(window.hwnd) && i != target_monitor_idx {
|
||||
moved_across_monitors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if moved_across_monitors {
|
||||
break;
|
||||
if let Some((m_idx, _)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if *m_idx != target_monitor_idx {
|
||||
moved_across_monitors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,40 +709,8 @@ impl WindowManager {
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
}
|
||||
|
||||
tracing::trace!("updating list of known hwnds");
|
||||
let mut known_hwnds = vec![];
|
||||
for monitor in self.monitors() {
|
||||
for workspace in monitor.workspaces() {
|
||||
for container in workspace.containers() {
|
||||
for window in container.windows() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(window) = workspace.maximized_window() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
for window in container.windows() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(hwnd_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||
// Update list of known_hwnds and their monitor/workspace index pair
|
||||
self.update_known_hwnds();
|
||||
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
|
||||
@@ -1,14 +1,164 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::notify_subscribers;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::NotificationEvent;
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowManagerEvent;
|
||||
use crate::DATA_DIR;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
|
||||
lazy_static! {
|
||||
pub static ref HWNDS_CACHE: Arc<Mutex<HashMap<isize, (usize, usize)>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub struct ReaperNotification(pub HashMap<isize, (usize, usize)>);
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<ReaperNotification>, Receiver<ReaperNotification>)> =
|
||||
OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<ReaperNotification>, Receiver<ReaperNotification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<ReaperNotification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<ReaperNotification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification(hwnds: HashMap<isize, (usize, usize)>) {
|
||||
if event_tx().try_send(ReaperNotification(hwnds)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
watch_for_orphans(wm.clone());
|
||||
|
||||
std::thread::spawn(move || loop {
|
||||
match find_orphans(wm.clone()) {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
for notification in receiver {
|
||||
let orphan_hwnds = notification.0;
|
||||
let mut wm = wm.lock();
|
||||
let offset = wm.work_area_offset;
|
||||
|
||||
let mut update_borders = false;
|
||||
|
||||
for (hwnd, (m_idx, w_idx)) in orphan_hwnds.iter() {
|
||||
if let Some(monitor) = wm.monitors_mut().get_mut(*m_idx) {
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
let work_area = *monitor.work_area_size();
|
||||
let window_based_work_area_offset = (
|
||||
monitor.window_based_work_area_offset_limit(),
|
||||
monitor.window_based_work_area_offset(),
|
||||
);
|
||||
|
||||
let offset = if monitor.work_area_offset().is_some() {
|
||||
monitor.work_area_offset()
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
|
||||
if let Some(workspace) = monitor.workspaces_mut().get_mut(*w_idx) {
|
||||
// Remove orphan window
|
||||
if let Err(error) = workspace.remove_window(*hwnd) {
|
||||
tracing::warn!(
|
||||
"error reaping orphan window ({}) on monitor: {}, workspace: {}. Error: {}",
|
||||
hwnd,
|
||||
m_idx,
|
||||
w_idx,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if focused_workspace_idx == *w_idx {
|
||||
// If this is not a focused workspace there is no need to update the
|
||||
// workspace or the borders. That will already be done when the user
|
||||
// changes to this workspace.
|
||||
workspace.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
update_borders = true;
|
||||
}
|
||||
tracing::info!(
|
||||
"reaped orphan window ({}) on monitor: {}, workspace: {}",
|
||||
hwnd,
|
||||
m_idx,
|
||||
w_idx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wm.known_hwnds.remove(hwnd);
|
||||
|
||||
let window = Window::from(*hwnd);
|
||||
notify_subscribers(
|
||||
crate::Notification {
|
||||
event: NotificationEvent::WindowManager(WindowManagerEvent::Destroy(
|
||||
WinEvent::ObjectDestroy,
|
||||
window,
|
||||
)),
|
||||
state: wm.as_ref().into(),
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
|
||||
if update_borders {
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
// Save to file
|
||||
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(hwnd_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &wm.known_hwnds.keys().collect::<Vec<_>>())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
|
||||
// Cache current hwnds
|
||||
{
|
||||
let mut cache = HWNDS_CACHE.lock();
|
||||
*cache = wm.lock().known_hwnds.clone();
|
||||
}
|
||||
|
||||
std::thread::spawn(move || loop {
|
||||
match find_orphans() {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
@@ -23,50 +173,37 @@ pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
fn find_orphans() -> color_eyre::Result<()> {
|
||||
tracing::info!("watching");
|
||||
|
||||
let arc = wm.clone();
|
||||
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
|
||||
let mut wm = arc.lock();
|
||||
let offset = wm.work_area_offset;
|
||||
let mut cache = HWNDS_CACHE.lock();
|
||||
let mut orphan_hwnds = HashMap::new();
|
||||
|
||||
let mut update_borders = false;
|
||||
for (hwnd, (m_idx, w_idx)) in cache.iter() {
|
||||
let window = Window::from(*hwnd);
|
||||
|
||||
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let work_area = *monitor.work_area_size();
|
||||
let window_based_work_area_offset = (
|
||||
monitor.window_based_work_area_offset_limit(),
|
||||
monitor.window_based_work_area_offset(),
|
||||
);
|
||||
|
||||
let offset = if monitor.work_area_offset().is_some() {
|
||||
monitor.work_area_offset()
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
|
||||
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
let reaped_orphans = workspace.reap_orphans()?;
|
||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||
workspace.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
update_borders = true;
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
reaped_orphans.1,
|
||||
i,
|
||||
j
|
||||
);
|
||||
}
|
||||
if !window.is_window()
|
||||
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
|
||||
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
|
||||
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
|
||||
// docs is closed
|
||||
//
|
||||
// I hate every single person who worked on Microsoft Office 365, especially Word
|
||||
|| !window.is_visible()
|
||||
{
|
||||
orphan_hwnds.insert(window.hwnd, (*m_idx, *w_idx));
|
||||
}
|
||||
}
|
||||
|
||||
if update_borders {
|
||||
border_manager::send_notification(None);
|
||||
if !orphan_hwnds.is_empty() {
|
||||
// Update reaper cache
|
||||
cache.retain(|h, _| !orphan_hwnds.contains_key(h));
|
||||
|
||||
// Send handles to remove
|
||||
event_tx().send(ReaperNotification(orphan_hwnds))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ impl Stackbar {
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
HINSTANCE(windows_api::as_ptr!(instance)),
|
||||
Option::from(HINSTANCE(windows_api::as_ptr!(instance))),
|
||||
None,
|
||||
)?;
|
||||
|
||||
@@ -133,7 +133,7 @@ impl Stackbar {
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
loop {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
tracing::debug!("stackbar window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
@@ -183,13 +183,13 @@ impl Stackbar {
|
||||
WindowsApi::position_window(self.hwnd, &layout, false)?;
|
||||
|
||||
unsafe {
|
||||
let hdc = GetDC(self.hwnd());
|
||||
let hdc = GetDC(Option::from(self.hwnd()));
|
||||
|
||||
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
|
||||
let hbrush = CreateSolidBrush(COLORREF(background));
|
||||
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
SelectObject(hdc, hpen.into());
|
||||
SelectObject(hdc, hbrush.into());
|
||||
SetBkColor(hdc, COLORREF(background));
|
||||
|
||||
let mut logfont = LOGFONTW {
|
||||
@@ -209,14 +209,14 @@ impl Stackbar {
|
||||
let logical_height = -MulDiv(
|
||||
STACKBAR_FONT_SIZE.load(Ordering::SeqCst),
|
||||
72,
|
||||
GetDeviceCaps(hdc, LOGPIXELSY),
|
||||
GetDeviceCaps(Option::from(hdc), LOGPIXELSY),
|
||||
);
|
||||
|
||||
logfont.lfHeight = logical_height;
|
||||
|
||||
let hfont = CreateFontIndirectW(&logfont);
|
||||
|
||||
SelectObject(hdc, hfont);
|
||||
SelectObject(hdc, hfont.into());
|
||||
|
||||
for (i, window) in container.windows().iter().enumerate() {
|
||||
if window.hwnd == container.focused_window().copied().unwrap_or_default().hwnd {
|
||||
@@ -283,13 +283,13 @@ impl Stackbar {
|
||||
);
|
||||
}
|
||||
|
||||
ReleaseDC(self.hwnd(), hdc);
|
||||
ReleaseDC(Option::from(self.hwnd()), hdc);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hpen);
|
||||
let _ = DeleteObject(hpen.into());
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hbrush);
|
||||
let _ = DeleteObject(hbrush.into());
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hfont);
|
||||
let _ = DeleteObject(hfont.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::core::BorderImplementation;
|
||||
use crate::core::StackbarLabel;
|
||||
use crate::core::StackbarMode;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator;
|
||||
use crate::ring::Ring;
|
||||
@@ -35,13 +36,16 @@ use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::AspectRatio;
|
||||
use crate::Axis;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::PredefinedAspectRatio;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::FLOATING_WINDOW_TOGGLE_ASPECT_RATIO;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
@@ -49,6 +53,7 @@ use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
|
||||
use crate::SLOW_APPLICATION_IDENTIFIERS;
|
||||
@@ -97,21 +102,26 @@ use std::sync::Arc;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct BorderColours {
|
||||
/// Border colour when the container contains a single window
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub single: Option<Colour>,
|
||||
/// Border colour when the container contains multiple windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stack: Option<Colour>,
|
||||
/// Border colour when the container is in monocle mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monocle: Option<Colour>,
|
||||
/// Border colour when the container is in floating mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating: Option<Colour>,
|
||||
/// Border colour when the container is unfocused
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unfocused: Option<Colour>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct WorkspaceConfig {
|
||||
/// Name
|
||||
pub name: String,
|
||||
@@ -121,7 +131,7 @@ pub struct WorkspaceConfig {
|
||||
/// END OF LIFE FEATURE: Custom Layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_layout: Option<PathBuf>,
|
||||
/// Layout rules (default: None)
|
||||
/// Layout rules in the format of threshold => layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
|
||||
/// END OF LIFE FEATURE: Custom layout rules (default: None)
|
||||
@@ -145,8 +155,10 @@ pub struct WorkspaceConfig {
|
||||
/// Determine what happens when a new window is opened (default: Create)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
/// (default: false)
|
||||
/// Window container behaviour rules in the format of threshold => behaviour (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_container_behaviour_rules: Option<HashMap<usize, WindowContainerBehaviour>>,
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_override: Option<bool>,
|
||||
/// Specify an axis on which to flip the selected layout (default: None)
|
||||
@@ -165,6 +177,12 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
Layout::Custom(_) => {}
|
||||
}
|
||||
}
|
||||
let layout_rules = (!layout_rules.is_empty()).then_some(layout_rules);
|
||||
|
||||
let mut window_container_behaviour_rules = HashMap::new();
|
||||
for (threshold, behaviour) in value.window_container_behaviour_rules().iter().flatten() {
|
||||
window_container_behaviour_rules.insert(*threshold, *behaviour);
|
||||
}
|
||||
|
||||
let default_container_padding = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
|
||||
let default_workspace_padding = DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst);
|
||||
@@ -190,28 +208,42 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
.name()
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("unnamed")),
|
||||
layout: match value.layout() {
|
||||
Layout::Default(layout) => Option::from(*layout),
|
||||
// TODO: figure out how we might resolve file references in the future
|
||||
Layout::Custom(_) => None,
|
||||
},
|
||||
custom_layout: None,
|
||||
layout_rules: Option::from(layout_rules),
|
||||
// TODO: figure out how we might resolve file references in the future
|
||||
custom_layout_rules: None,
|
||||
layout: value
|
||||
.tile()
|
||||
.then_some(match value.layout() {
|
||||
Layout::Default(layout) => Option::from(*layout),
|
||||
Layout::Custom(_) => None,
|
||||
})
|
||||
.flatten(),
|
||||
custom_layout: value
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.custom_layout.clone()),
|
||||
layout_rules,
|
||||
custom_layout_rules: value
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.custom_layout_rules.clone()),
|
||||
container_padding,
|
||||
workspace_padding,
|
||||
initial_workspace_rules: None,
|
||||
workspace_rules: None,
|
||||
initial_workspace_rules: value
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.initial_workspace_rules.clone()),
|
||||
workspace_rules: value
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.workspace_rules.clone()),
|
||||
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
|
||||
window_container_behaviour: *value.window_container_behaviour(),
|
||||
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
|
||||
float_override: *value.float_override(),
|
||||
layout_flip: value.layout_flip(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct MonitorConfig {
|
||||
/// Workspace configurations
|
||||
pub workspaces: Vec<WorkspaceConfig>,
|
||||
@@ -242,8 +274,8 @@ impl From<&Monitor> for MonitorConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.33`
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.35`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -354,6 +386,9 @@ pub struct StaticConfig {
|
||||
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change_applications: Option<Vec<MatchingRule>>,
|
||||
/// Do not process EVENT_OBJECT_NAMECHANGE events as Show events for identified applications matching these title regexes
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change_title_ignore_list: Option<Vec<String>>,
|
||||
/// Set monitor index preferences
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
|
||||
@@ -376,26 +411,33 @@ pub struct StaticConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slow_application_compensation_time: Option<u64>,
|
||||
/// Komorebi status bar configuration files for multiple instances on different monitors
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
// this option is a little special because it is only consumed by komorebic
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bar_configurations: Option<Vec<PathBuf>>,
|
||||
/// HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub remove_titlebar_applications: Option<Vec<MatchingRule>>,
|
||||
/// Aspect ratio to resize with when toggling floating mode for a window
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating_window_aspect_ratio: Option<AspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct AnimationsConfig {
|
||||
/// Enable or disable animations (default: false)
|
||||
enabled: PerAnimationPrefixConfig<bool>,
|
||||
pub enabled: PerAnimationPrefixConfig<bool>,
|
||||
/// Set the animation duration in ms (default: 250)
|
||||
duration: Option<PerAnimationPrefixConfig<u64>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<PerAnimationPrefixConfig<u64>>,
|
||||
/// Set the animation style (default: Linear)
|
||||
style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
|
||||
/// Set the animation FPS (default: 60)
|
||||
fps: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fps: Option<u64>,
|
||||
}
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomorebiTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
@@ -403,45 +445,63 @@ pub enum KomorebiTheme {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
/// Border colour when the container contains a single window (default: Blue)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
single_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container contains multiple windows (default: Green)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stack_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container is in monocle mode (default: Pink)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
monocle_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the window is floating (default: Yellow)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
floating_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container is unfocused (default: Base)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Stackbar focused tab text colour (default: Green)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Stackbar unfocused tab text colour (default: Text)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_unfocused_text: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Stackbar tab background colour (default: Base)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_background: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Komorebi status bar accent (default: Blue)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
bar_accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
name: komorebi_themes::Base16,
|
||||
/// Border colour when the container contains a single window (default: Base0D)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
single_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container contains multiple windows (default: Base0B)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stack_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is in monocle mode (default: Base0F)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
monocle_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the window is floating (default: Base09)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
floating_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused (default: Base01)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar focused tab text colour (default: Base0B)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar unfocused tab text colour (default: Base05)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar tab background colour (default: Base01)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_background: Option<komorebi_themes::Base16Value>,
|
||||
/// Komorebi status bar accent (default: Base0D)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
bar_accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
}
|
||||
@@ -527,31 +587,41 @@ impl StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct TabsConfig {
|
||||
/// Width of a stackbar tab
|
||||
width: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub width: Option<i32>,
|
||||
/// Focused tab text colour
|
||||
focused_text: Option<Colour>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub focused_text: Option<Colour>,
|
||||
/// Unfocused tab text colour
|
||||
unfocused_text: Option<Colour>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unfocused_text: Option<Colour>,
|
||||
/// Tab background colour
|
||||
background: Option<Colour>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<Colour>,
|
||||
/// Font family
|
||||
font_family: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub font_family: Option<String>,
|
||||
/// Font size
|
||||
font_size: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub font_size: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct StackbarConfig {
|
||||
/// Stackbar height
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub height: Option<i32>,
|
||||
/// Stackbar label
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub label: Option<StackbarLabel>,
|
||||
/// Stackbar mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<StackbarMode>,
|
||||
/// Stackbar tab configuration options
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tabs: Option<TabsConfig>,
|
||||
}
|
||||
|
||||
@@ -625,7 +695,17 @@ impl From<&WindowManager> for StaticConfig {
|
||||
border_overflow_applications: None,
|
||||
tray_and_multi_window_applications: None,
|
||||
layered_applications: None,
|
||||
object_name_change_applications: None,
|
||||
object_name_change_applications: Option::from(
|
||||
OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
|
||||
),
|
||||
object_name_change_title_ignore_list: Option::from(
|
||||
OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST
|
||||
.lock()
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|r| r.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
|
||||
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
|
||||
stackbar: None,
|
||||
@@ -637,6 +717,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
slow_application_identifiers: Option::from(SLOW_APPLICATION_IDENTIFIERS.lock().clone()),
|
||||
bar_configurations: None,
|
||||
remove_titlebar_applications: Option::from(NO_TITLEBAR.lock().clone()),
|
||||
floating_window_aspect_ratio: Option::from(*FLOATING_WINDOW_TOGGLE_ASPECT_RATIO.lock()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -644,6 +725,10 @@ impl From<&WindowManager> for StaticConfig {
|
||||
impl StaticConfig {
|
||||
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
||||
fn apply_globals(&mut self) -> Result<()> {
|
||||
*FLOATING_WINDOW_TOGGLE_ASPECT_RATIO.lock() = self
|
||||
.floating_window_aspect_ratio
|
||||
.unwrap_or(AspectRatio::Predefined(PredefinedAspectRatio::Standard));
|
||||
|
||||
if let Some(monitor_index_preferences) = &self.monitor_index_preferences {
|
||||
let mut preferences = MONITOR_INDEX_PREFERENCES.lock();
|
||||
preferences.clone_from(monitor_index_preferences);
|
||||
@@ -779,6 +864,7 @@ impl StaticConfig {
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
let mut object_name_change_title_ignore_list = OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST.lock();
|
||||
let mut layered_identifiers = LAYERED_WHITELIST.lock();
|
||||
let mut transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
|
||||
let mut slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
|
||||
@@ -805,6 +891,17 @@ impl StaticConfig {
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(regexes) = &mut self.object_name_change_title_ignore_list {
|
||||
let mut updated = vec![];
|
||||
for r in regexes {
|
||||
if let Ok(regex) = Regex::new(r) {
|
||||
updated.push(regex);
|
||||
}
|
||||
}
|
||||
|
||||
*object_name_change_title_ignore_list = updated;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut self.layered_applications {
|
||||
populate_rules(rules, &mut layered_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
@@ -1014,6 +1111,10 @@ impl StaticConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_raw(raw: &str) -> Result<Self> {
|
||||
Ok(serde_json::from_str(raw)?)
|
||||
}
|
||||
|
||||
pub fn read(path: &PathBuf) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
@@ -1078,6 +1179,7 @@ impl StaticConfig {
|
||||
|
||||
let mut wm = WindowManager {
|
||||
monitors: Ring::default(),
|
||||
monitor_usr_idx_map: HashMap::new(),
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
@@ -1106,6 +1208,7 @@ impl StaticConfig {
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloack_to_ignore: 0,
|
||||
known_hwnds: HashMap::new(),
|
||||
};
|
||||
|
||||
match value.focus_follows_mouse {
|
||||
@@ -1139,32 +1242,78 @@ impl StaticConfig {
|
||||
let value = Self::read(path)?;
|
||||
let mut wm = wm.lock();
|
||||
|
||||
if let Some(monitors) = value.monitors {
|
||||
for (i, monitor) in monitors.iter().enumerate() {
|
||||
{
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
if let Some(device_id) = display_index_preferences.get(&i) {
|
||||
monitor_reconciliator::insert_in_monitor_cache(device_id, monitor.clone());
|
||||
let configs_with_preference: Vec<_> =
|
||||
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
|
||||
let mut configs_used = Vec::new();
|
||||
|
||||
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
workspace_matching_rules.clear();
|
||||
drop(workspace_matching_rules);
|
||||
|
||||
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let preferred_config_idx = {
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
|
||||
(monitor
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.is_some_and(|sn| sn == id)
|
||||
|| monitor.device_id() == id)
|
||||
.then_some(*c_idx)
|
||||
});
|
||||
c_idx
|
||||
};
|
||||
let idx = preferred_config_idx.or({
|
||||
// Monitor without preferred config idx.
|
||||
// Get index of first config that is not a preferred config of some other monitor
|
||||
// and that has not been used yet. This might return `None` as well, in that case
|
||||
// this monitor won't have a config tied to it and will use the default values.
|
||||
let m_config_count = value
|
||||
.monitors
|
||||
.as_ref()
|
||||
.map(|ms| ms.len())
|
||||
.unwrap_or_default();
|
||||
(0..m_config_count)
|
||||
.find(|i| !configs_with_preference.contains(i) && !configs_used.contains(i))
|
||||
});
|
||||
if let Some(monitor_config) = value
|
||||
.monitors
|
||||
.as_ref()
|
||||
.and_then(|ms| idx.and_then(|i| ms.get(i)))
|
||||
{
|
||||
if let Some(used_config_idx) = idx {
|
||||
configs_used.push(used_config_idx);
|
||||
}
|
||||
|
||||
monitor.ensure_workspace_count(monitor_config.workspaces.len());
|
||||
monitor.set_work_area_offset(monitor_config.work_area_offset);
|
||||
monitor.set_window_based_work_area_offset(
|
||||
monitor_config.window_based_work_area_offset,
|
||||
);
|
||||
monitor.set_window_based_work_area_offset_limit(
|
||||
monitor_config
|
||||
.window_based_work_area_offset_limit
|
||||
.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(m) = wm.monitors_mut().get_mut(i) {
|
||||
m.ensure_workspace_count(monitor.workspaces.len());
|
||||
m.set_work_area_offset(monitor.work_area_offset);
|
||||
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
monitor.window_based_work_area_offset_limit.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
// Check if this monitor config is the preferred config for this monitor and store
|
||||
// a copy of the monitor itself on the monitor cache if it is.
|
||||
if idx == preferred_config_idx {
|
||||
let id = monitor
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.map_or(monitor.device_id(), |sn| sn);
|
||||
monitor_reconciliator::insert_in_monitor_cache(id, monitor.clone());
|
||||
}
|
||||
|
||||
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
for (j, ws) in monitor.workspaces.iter().enumerate() {
|
||||
for (j, ws) in monitor_config.workspaces.iter().enumerate() {
|
||||
if let Some(rules) = &ws.workspace_rules {
|
||||
for r in rules {
|
||||
workspace_matching_rules.push(WorkspaceMatchingRule {
|
||||
@@ -1190,6 +1339,56 @@ impl StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for configs that should be tied to a specific display that isn't loaded right now
|
||||
// and cache a monitor with those configs with the specific `serial_number_id` so that when
|
||||
// those devices are connected later we can use the correct config from the cache.
|
||||
if configs_with_preference.len() > configs_used.len() {
|
||||
for i in configs_with_preference
|
||||
.iter()
|
||||
.filter(|i| !configs_used.contains(i))
|
||||
{
|
||||
let id = {
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
display_index_preferences.get(i).cloned()
|
||||
};
|
||||
if let (Some(id), Some(monitor_config)) =
|
||||
(id, value.monitors.as_ref().and_then(|ms| ms.get(*i)))
|
||||
{
|
||||
// The name, device, device_id and serial_number_id can be empty here since
|
||||
// once the monitor with this preferred index actually connects the
|
||||
// `load_monitor_information` function will update these fields.
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"".into(),
|
||||
"".into(),
|
||||
"".into(),
|
||||
None,
|
||||
);
|
||||
|
||||
m.ensure_workspace_count(monitor_config.workspaces.len());
|
||||
m.set_work_area_offset(monitor_config.work_area_offset);
|
||||
m.set_window_based_work_area_offset(
|
||||
monitor_config.window_based_work_area_offset,
|
||||
);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
monitor_config
|
||||
.window_based_work_area_offset_limit
|
||||
.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
|
||||
monitor_reconciliator::insert_in_monitor_cache(&id, m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wm.enforce_workspace_rules()?;
|
||||
|
||||
if value.border == Some(true) {
|
||||
@@ -1204,28 +1403,80 @@ impl StaticConfig {
|
||||
|
||||
value.apply_globals()?;
|
||||
|
||||
if let Some(monitors) = value.monitors {
|
||||
for (i, monitor) in monitors.iter().enumerate() {
|
||||
if let Some(m) = wm.monitors_mut().get_mut(i) {
|
||||
m.ensure_workspace_count(monitor.workspaces.len());
|
||||
if m.work_area_offset().is_none() {
|
||||
m.set_work_area_offset(monitor.work_area_offset);
|
||||
}
|
||||
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
monitor.window_based_work_area_offset_limit.unwrap_or(1),
|
||||
);
|
||||
let configs_with_preference: Vec<_> =
|
||||
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
|
||||
let mut configs_used = Vec::new();
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
workspace_matching_rules.clear();
|
||||
drop(workspace_matching_rules);
|
||||
|
||||
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let preferred_config_idx = {
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
|
||||
(monitor
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.is_some_and(|sn| sn == id)
|
||||
|| monitor.device_id() == id)
|
||||
.then_some(*c_idx)
|
||||
});
|
||||
c_idx
|
||||
};
|
||||
let idx = preferred_config_idx.or({
|
||||
// Monitor without preferred config idx.
|
||||
// Get index of first config that is not a preferred config of some other monitor
|
||||
// and that has not been used yet. This might return `None` as well, in that case
|
||||
// this monitor won't have a config tied to it and will use the default values.
|
||||
let m_config_count = value
|
||||
.monitors
|
||||
.as_ref()
|
||||
.map(|ms| ms.len())
|
||||
.unwrap_or_default();
|
||||
(0..m_config_count)
|
||||
.find(|i| !configs_with_preference.contains(i) && !configs_used.contains(i))
|
||||
});
|
||||
if let Some(monitor_config) = value
|
||||
.monitors
|
||||
.as_ref()
|
||||
.and_then(|ms| idx.and_then(|i| ms.get(i)))
|
||||
{
|
||||
if let Some(used_config_idx) = idx {
|
||||
configs_used.push(used_config_idx);
|
||||
}
|
||||
|
||||
monitor.ensure_workspace_count(monitor_config.workspaces.len());
|
||||
if monitor.work_area_offset().is_none() {
|
||||
monitor.set_work_area_offset(monitor_config.work_area_offset);
|
||||
}
|
||||
monitor.set_window_based_work_area_offset(
|
||||
monitor_config.window_based_work_area_offset,
|
||||
);
|
||||
monitor.set_window_based_work_area_offset_limit(
|
||||
monitor_config
|
||||
.window_based_work_area_offset_limit
|
||||
.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this monitor config is the preferred config for this monitor and store
|
||||
// a copy of the monitor itself on the monitor cache if it is.
|
||||
if idx == preferred_config_idx {
|
||||
let id = monitor
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.map_or(monitor.device_id(), |sn| sn);
|
||||
monitor_reconciliator::insert_in_monitor_cache(id, monitor.clone());
|
||||
}
|
||||
|
||||
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
workspace_matching_rules.clear();
|
||||
for (j, ws) in monitor.workspaces.iter().enumerate() {
|
||||
for (j, ws) in monitor_config.workspaces.iter().enumerate() {
|
||||
if let Some(rules) = &ws.workspace_rules {
|
||||
for r in rules {
|
||||
workspace_matching_rules.push(WorkspaceMatchingRule {
|
||||
@@ -1251,6 +1502,56 @@ impl StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for configs that should be tied to a specific display that isn't loaded right now
|
||||
// and cache a monitor with those configs with the specific `serial_number_id` so that when
|
||||
// those devices are connected later we can use the correct config from the cache.
|
||||
if configs_with_preference.len() > configs_used.len() {
|
||||
for i in configs_with_preference
|
||||
.iter()
|
||||
.filter(|i| !configs_used.contains(i))
|
||||
{
|
||||
let id = {
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
display_index_preferences.get(i).cloned()
|
||||
};
|
||||
if let (Some(id), Some(monitor_config)) =
|
||||
(id, value.monitors.as_ref().and_then(|ms| ms.get(*i)))
|
||||
{
|
||||
// The name, device, device_id and serial_number_id can be empty here since
|
||||
// once the monitor with this preferred index actually connects the
|
||||
// `load_monitor_information` function will update these fields.
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"".into(),
|
||||
"".into(),
|
||||
"".into(),
|
||||
None,
|
||||
);
|
||||
|
||||
m.ensure_workspace_count(monitor_config.workspaces.len());
|
||||
m.set_work_area_offset(monitor_config.work_area_offset);
|
||||
m.set_window_based_work_area_offset(
|
||||
monitor_config.window_based_work_area_offset,
|
||||
);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
monitor_config
|
||||
.window_based_work_area_offset_limit
|
||||
.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
|
||||
monitor_reconciliator::insert_in_monitor_cache(&id, m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wm.enforce_workspace_rules()?;
|
||||
|
||||
if let Some(enabled) = value.border {
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::focus_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::windows_api;
|
||||
use crate::AnimationStyle;
|
||||
use crate::FLOATING_WINDOW_TOGGLE_ASPECT_RATIO;
|
||||
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
|
||||
use crate::SLOW_APPLICATION_IDENTIFIERS;
|
||||
use std::collections::HashMap;
|
||||
@@ -39,6 +40,8 @@ use serde::ser::SerializeStruct;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use crate::core::ApplicationIdentifier;
|
||||
@@ -296,6 +299,49 @@ impl RenderDispatcher for TransparencyRenderDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
|
||||
)]
|
||||
#[serde(untagged)]
|
||||
pub enum AspectRatio {
|
||||
/// A predefined aspect ratio
|
||||
Predefined(PredefinedAspectRatio),
|
||||
/// A custom W:H aspect ratio
|
||||
Custom(i32, i32),
|
||||
}
|
||||
|
||||
impl Default for AspectRatio {
|
||||
fn default() -> Self {
|
||||
AspectRatio::Predefined(PredefinedAspectRatio::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
|
||||
)]
|
||||
pub enum PredefinedAspectRatio {
|
||||
/// 21:9
|
||||
Ultrawide,
|
||||
/// 16:9
|
||||
Widescreen,
|
||||
/// 4:3
|
||||
#[default]
|
||||
Standard,
|
||||
}
|
||||
|
||||
impl AspectRatio {
|
||||
pub fn width_and_height(self) -> (i32, i32) {
|
||||
match self {
|
||||
AspectRatio::Predefined(predefined) => match predefined {
|
||||
PredefinedAspectRatio::Ultrawide => (21, 9),
|
||||
PredefinedAspectRatio::Widescreen => (16, 9),
|
||||
PredefinedAspectRatio::Standard => (4, 3),
|
||||
},
|
||||
AspectRatio::Custom(w, h) => (w, h),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
@@ -369,15 +415,21 @@ impl Window {
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
let (aspect_ratio_width, aspect_ratio_height) = FLOATING_WINDOW_TOGGLE_ASPECT_RATIO
|
||||
.lock()
|
||||
.width_and_height();
|
||||
let target_height = work_area.bottom / 2;
|
||||
let target_width = (target_height * aspect_ratio_width) / aspect_ratio_height;
|
||||
|
||||
let x = work_area.left + ((work_area.right - target_width) / 2);
|
||||
let y = work_area.top + ((work_area.bottom - target_height) / 2);
|
||||
|
||||
self.set_position(
|
||||
&Rect {
|
||||
left: work_area.left + ((work_area.right - half_width) / 2),
|
||||
top: work_area.top + ((work_area.bottom - half_weight) / 2),
|
||||
right: half_width,
|
||||
bottom: half_weight,
|
||||
left: x,
|
||||
top: y,
|
||||
right: target_width,
|
||||
bottom: target_height,
|
||||
},
|
||||
true,
|
||||
)
|
||||
@@ -872,7 +924,11 @@ fn window_is_eligible(
|
||||
|
||||
let known_layered_hwnds = transparency_manager::known_hwnds();
|
||||
|
||||
allow_layered = if known_layered_hwnds.contains(&hwnd) {
|
||||
allow_layered = if known_layered_hwnds.contains(&hwnd)
|
||||
// we always want to process hide events for windows with transparency, even on other
|
||||
// monitors, because we don't want to be left with ghost tiles
|
||||
|| matches!(event, Some(WindowManagerEvent::Hide(_, _)))
|
||||
{
|
||||
debug.allow_layered_transparency = true;
|
||||
true
|
||||
} else {
|
||||
@@ -924,12 +980,12 @@ fn window_is_eligible(
|
||||
}
|
||||
|
||||
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
|
||||
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
|
||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||
// on FocusChange events if I don't filter out this one. But, if we are
|
||||
// allowing a specific layered window on the whitelist (like Steam), it should
|
||||
// pass this check
|
||||
&& (allow_layered || !ex_style.contains(ExtendedWindowStyle::LAYERED))
|
||||
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
|
||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||
// on FocusChange events if I don't filter out this one. But, if we are
|
||||
// allowing a specific layered window on the whitelist (like Steam), it should
|
||||
// pass this check
|
||||
&& (allow_layered || !ex_style.contains(ExtendedWindowStyle::LAYERED))
|
||||
|| managed_override
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::env::temp_dir;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::Shutdown;
|
||||
use std::num::NonZeroUsize;
|
||||
@@ -48,6 +49,8 @@ use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowManagementBehaviour;
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::container::Container;
|
||||
@@ -74,6 +77,7 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::BorderColours;
|
||||
use crate::Colour;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
@@ -99,6 +103,7 @@ use crate::WORKSPACE_MATCHING_RULES;
|
||||
#[derive(Debug)]
|
||||
pub struct WindowManager {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub monitor_usr_idx_map: HashMap<usize, usize>,
|
||||
pub incoming_events: Receiver<WindowManagerEvent>,
|
||||
pub command_listener: UnixListener,
|
||||
pub is_paused: bool,
|
||||
@@ -116,12 +121,15 @@ pub struct WindowManager {
|
||||
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
|
||||
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
|
||||
pub uncloack_to_ignore: usize,
|
||||
/// Maps each known window hwnd to the (monitor, workspace) index pair managing it
|
||||
pub known_hwnds: HashMap<isize, (usize, usize)>,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub monitor_usr_idx_map: HashMap<usize, usize>,
|
||||
pub is_paused: bool,
|
||||
pub resize_delta: i32,
|
||||
pub new_window_behaviour: WindowContainerBehaviour,
|
||||
@@ -281,8 +289,69 @@ impl AsRef<Self> for WindowManager {
|
||||
|
||||
impl From<&WindowManager> for State {
|
||||
fn from(wm: &WindowManager) -> Self {
|
||||
// This is used to remove any information that doesn't need to be passed on to subscribers
|
||||
// or to be shown with the `komorebic state` command. Currently it is only removing the
|
||||
// `workspace_config` field from every workspace, but more stripping can be added later if
|
||||
// needed.
|
||||
let mut stripped_monitors = Ring::default();
|
||||
*stripped_monitors.elements_mut() = wm
|
||||
.monitors()
|
||||
.iter()
|
||||
.map(|monitor| Monitor {
|
||||
id: monitor.id,
|
||||
name: monitor.name.clone(),
|
||||
device: monitor.device.clone(),
|
||||
device_id: monitor.device_id.clone(),
|
||||
serial_number_id: monitor.serial_number_id.clone(),
|
||||
size: monitor.size,
|
||||
work_area_size: monitor.work_area_size,
|
||||
work_area_offset: monitor.work_area_offset,
|
||||
window_based_work_area_offset: monitor.window_based_work_area_offset,
|
||||
window_based_work_area_offset_limit: monitor.window_based_work_area_offset_limit,
|
||||
workspaces: {
|
||||
let mut ws = Ring::default();
|
||||
*ws.elements_mut() = monitor
|
||||
.workspaces()
|
||||
.iter()
|
||||
.map(|workspace| Workspace {
|
||||
name: workspace.name.clone(),
|
||||
containers: workspace.containers.clone(),
|
||||
monocle_container: workspace.monocle_container.clone(),
|
||||
monocle_container_restore_idx: workspace.monocle_container_restore_idx,
|
||||
maximized_window: workspace.maximized_window,
|
||||
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
|
||||
floating_windows: workspace.floating_windows.clone(),
|
||||
layout: workspace.layout.clone(),
|
||||
layout_rules: workspace.layout_rules.clone(),
|
||||
layout_flip: workspace.layout_flip,
|
||||
workspace_padding: workspace.workspace_padding,
|
||||
container_padding: workspace.container_padding,
|
||||
latest_layout: workspace.latest_layout.clone(),
|
||||
resize_dimensions: workspace.resize_dimensions.clone(),
|
||||
tile: workspace.tile,
|
||||
apply_window_based_work_area_offset: workspace
|
||||
.apply_window_based_work_area_offset,
|
||||
window_container_behaviour: workspace.window_container_behaviour,
|
||||
window_container_behaviour_rules: workspace
|
||||
.window_container_behaviour_rules
|
||||
.clone(),
|
||||
float_override: workspace.float_override,
|
||||
layer: workspace.layer,
|
||||
workspace_config: None,
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
ws.focus(monitor.workspaces.focused_idx());
|
||||
ws
|
||||
},
|
||||
last_focused_workspace: monitor.last_focused_workspace,
|
||||
workspace_names: monitor.workspace_names.clone(),
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
stripped_monitors.focus(wm.monitors.focused_idx());
|
||||
|
||||
Self {
|
||||
monitors: wm.monitors.clone(),
|
||||
monitors: stripped_monitors,
|
||||
monitor_usr_idx_map: wm.monitor_usr_idx_map.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
work_area_offset: wm.work_area_offset,
|
||||
resize_delta: wm.resize_delta,
|
||||
@@ -343,6 +412,7 @@ impl WindowManager {
|
||||
|
||||
Ok(Self {
|
||||
monitors: Ring::default(),
|
||||
monitor_usr_idx_map: HashMap::new(),
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
@@ -360,13 +430,14 @@ impl WindowManager {
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloack_to_ignore: 0,
|
||||
known_hwnds: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn init(&mut self) -> Result<()> {
|
||||
tracing::info!("initialising");
|
||||
WindowsApi::load_monitor_information(&mut self.monitors)?;
|
||||
WindowsApi::load_monitor_information(self)?;
|
||||
WindowsApi::load_workspace_information(&mut self.monitors)
|
||||
}
|
||||
|
||||
@@ -1325,86 +1396,168 @@ impl WindowManager {
|
||||
delta: i32,
|
||||
update: bool,
|
||||
) -> Result<()> {
|
||||
let work_area = self.focused_monitor_work_area()?;
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let mut focused_monitor_work_area = self.focused_monitor_work_area()?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
tracing::info!("resizing window");
|
||||
let len = NonZeroUsize::new(workspace.containers().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one container"))?;
|
||||
let focused_idx = workspace.focused_container_idx();
|
||||
let focused_idx_resize = workspace
|
||||
.resize_dimensions()
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
|
||||
match workspace.layer() {
|
||||
WorkspaceLayer::Floating => {
|
||||
let workspace = self.focused_workspace()?;
|
||||
let focused_hwnd = WindowsApi::foreground_window()?;
|
||||
|
||||
if direction
|
||||
.destination(
|
||||
workspace.layout().as_boxed_direction().as_ref(),
|
||||
workspace.layout_flip(),
|
||||
focused_idx,
|
||||
len,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
let unaltered = layout.calculate(
|
||||
&work_area,
|
||||
len,
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
);
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
focused_monitor_work_area.left += border_offset;
|
||||
focused_monitor_work_area.left += border_width;
|
||||
focused_monitor_work_area.top += border_offset;
|
||||
focused_monitor_work_area.top += border_width;
|
||||
focused_monitor_work_area.right -= border_offset;
|
||||
focused_monitor_work_area.right -= border_width;
|
||||
focused_monitor_work_area.bottom -= border_offset;
|
||||
focused_monitor_work_area.bottom -= border_width;
|
||||
|
||||
let mut direction = direction;
|
||||
|
||||
// We only ever want to operate on the unflipped Rect positions when resizing, then we
|
||||
// can flip them however they need to be flipped once the resizing has been done
|
||||
if let Some(flip) = workspace.layout_flip() {
|
||||
match flip {
|
||||
Axis::Horizontal => {
|
||||
if matches!(direction, OperationDirection::Left)
|
||||
|| matches!(direction, OperationDirection::Right)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
for window in workspace.floating_windows().iter() {
|
||||
if window.hwnd == focused_hwnd {
|
||||
let mut rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
match (direction, sizing) {
|
||||
(OperationDirection::Left, Sizing::Increase) => {
|
||||
if rect.left - delta < focused_monitor_work_area.left {
|
||||
rect.left = focused_monitor_work_area.left;
|
||||
} else {
|
||||
rect.left -= delta;
|
||||
}
|
||||
}
|
||||
Axis::Vertical => {
|
||||
if matches!(direction, OperationDirection::Up)
|
||||
|| matches!(direction, OperationDirection::Down)
|
||||
(OperationDirection::Left, Sizing::Decrease) => {
|
||||
rect.left += delta;
|
||||
}
|
||||
(OperationDirection::Right, Sizing::Increase) => {
|
||||
if rect.left + rect.right + delta * 2
|
||||
> focused_monitor_work_area.right
|
||||
{
|
||||
direction = direction.opposite();
|
||||
rect.right = focused_monitor_work_area.right - rect.left;
|
||||
} else {
|
||||
rect.right += delta * 2;
|
||||
}
|
||||
}
|
||||
Axis::HorizontalAndVertical => direction = direction.opposite(),
|
||||
(OperationDirection::Right, Sizing::Decrease) => {
|
||||
rect.right -= delta * 2;
|
||||
}
|
||||
(OperationDirection::Up, Sizing::Increase) => {
|
||||
if rect.top - delta < focused_monitor_work_area.top {
|
||||
rect.top = focused_monitor_work_area.top;
|
||||
} else {
|
||||
rect.top -= delta;
|
||||
}
|
||||
}
|
||||
(OperationDirection::Up, Sizing::Decrease) => {
|
||||
rect.top += delta;
|
||||
}
|
||||
(OperationDirection::Down, Sizing::Increase) => {
|
||||
if rect.top + rect.bottom + delta * 2
|
||||
> focused_monitor_work_area.bottom
|
||||
{
|
||||
rect.bottom = focused_monitor_work_area.bottom - rect.top;
|
||||
} else {
|
||||
rect.bottom += delta * 2;
|
||||
}
|
||||
}
|
||||
(OperationDirection::Down, Sizing::Decrease) => {
|
||||
rect.bottom -= delta * 2;
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::position_window(window.hwnd, &rect, false)?;
|
||||
if mouse_follows_focus {
|
||||
WindowsApi::center_cursor_in_rect(&rect)?;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
let resize = layout.resize(
|
||||
unaltered
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no last layout"))?,
|
||||
focused_idx_resize,
|
||||
direction,
|
||||
sizing,
|
||||
delta,
|
||||
);
|
||||
|
||||
workspace.resize_dimensions_mut()[focused_idx] = resize;
|
||||
|
||||
return if update {
|
||||
self.update_focused_workspace(false, false)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
}
|
||||
|
||||
tracing::warn!("cannot resize container in this direction");
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
tracing::warn!("containers cannot be resized when using custom layouts");
|
||||
WorkspaceLayer::Tiling => {
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
tracing::info!("resizing window");
|
||||
let len = NonZeroUsize::new(workspace.containers().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one container"))?;
|
||||
let focused_idx = workspace.focused_container_idx();
|
||||
let focused_idx_resize = workspace
|
||||
.resize_dimensions()
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| {
|
||||
anyhow!("there is no resize adjustment for this container")
|
||||
})?;
|
||||
|
||||
if direction
|
||||
.destination(
|
||||
workspace.layout().as_boxed_direction().as_ref(),
|
||||
workspace.layout_flip(),
|
||||
focused_idx,
|
||||
len,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
let unaltered = layout.calculate(
|
||||
&focused_monitor_work_area,
|
||||
len,
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
);
|
||||
|
||||
let mut direction = direction;
|
||||
|
||||
// We only ever want to operate on the unflipped Rect positions when resizing, then we
|
||||
// can flip them however they need to be flipped once the resizing has been done
|
||||
if let Some(flip) = workspace.layout_flip() {
|
||||
match flip {
|
||||
Axis::Horizontal => {
|
||||
if matches!(direction, OperationDirection::Left)
|
||||
|| matches!(direction, OperationDirection::Right)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Axis::Vertical => {
|
||||
if matches!(direction, OperationDirection::Up)
|
||||
|| matches!(direction, OperationDirection::Down)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Axis::HorizontalAndVertical => direction = direction.opposite(),
|
||||
}
|
||||
}
|
||||
|
||||
let resize = layout.resize(
|
||||
unaltered
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no last layout"))?,
|
||||
focused_idx_resize,
|
||||
direction,
|
||||
sizing,
|
||||
delta,
|
||||
);
|
||||
|
||||
workspace.resize_dimensions_mut()[focused_idx] = resize;
|
||||
|
||||
return if update {
|
||||
self.update_focused_workspace(false, false)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
}
|
||||
|
||||
tracing::warn!("cannot resize container in this direction");
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
tracing::warn!("containers cannot be resized when using custom layouts");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1793,6 +1946,56 @@ impl WindowManager {
|
||||
self.update_focused_workspace(mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_floating_window_in_direction(
|
||||
&mut self,
|
||||
direction: OperationDirection,
|
||||
) -> Result<()> {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
|
||||
let mut target_idx = None;
|
||||
let len = focused_workspace.floating_windows().len();
|
||||
|
||||
if len > 1 {
|
||||
let focused_hwnd = WindowsApi::foreground_window()?;
|
||||
for (idx, window) in focused_workspace.floating_windows().iter().enumerate() {
|
||||
if window.hwnd == focused_hwnd {
|
||||
match direction {
|
||||
OperationDirection::Left => {}
|
||||
OperationDirection::Right => {}
|
||||
OperationDirection::Up => {
|
||||
if idx == len - 1 {
|
||||
target_idx = Some(0)
|
||||
} else {
|
||||
target_idx = Some(idx + 1)
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if idx == 0 {
|
||||
target_idx = Some(len - 1)
|
||||
} else {
|
||||
target_idx = Some(idx - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if target_idx.is_none() {
|
||||
target_idx = Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = target_idx {
|
||||
if let Some(window) = focused_workspace.floating_windows().get(idx) {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
@@ -1888,21 +2091,10 @@ impl WindowManager {
|
||||
if let Ok(focused_workspace) = self.focused_workspace_mut() {
|
||||
if let Some(window) = focused_workspace.maximized_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
// (alex-ds13): @LGUG2Z Why was this being done below on the monocle?
|
||||
// Should it really be done?
|
||||
//
|
||||
// WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
|
||||
// window.hwnd,
|
||||
// )?)?;
|
||||
|
||||
cross_monitor_monocle_or_max = true;
|
||||
} else if let Some(monocle) = focused_workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
|
||||
window.hwnd,
|
||||
)?)?;
|
||||
|
||||
cross_monitor_monocle_or_max = true;
|
||||
}
|
||||
} else {
|
||||
@@ -1949,6 +2141,75 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn move_floating_window_in_direction(
|
||||
&mut self,
|
||||
direction: OperationDirection,
|
||||
) -> Result<()> {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
|
||||
let mut focused_monitor_work_area = self.focused_monitor_work_area()?;
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
focused_monitor_work_area.left += border_offset;
|
||||
focused_monitor_work_area.left += border_width;
|
||||
focused_monitor_work_area.top += border_offset;
|
||||
focused_monitor_work_area.top += border_width;
|
||||
focused_monitor_work_area.right -= border_offset;
|
||||
focused_monitor_work_area.right -= border_width;
|
||||
focused_monitor_work_area.bottom -= border_offset;
|
||||
focused_monitor_work_area.bottom -= border_width;
|
||||
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
let delta = self.resize_delta;
|
||||
|
||||
let focused_hwnd = WindowsApi::foreground_window()?;
|
||||
for window in focused_workspace.floating_windows().iter() {
|
||||
if window.hwnd == focused_hwnd {
|
||||
let mut rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
if rect.left - delta < focused_monitor_work_area.left {
|
||||
rect.left = focused_monitor_work_area.left;
|
||||
} else {
|
||||
rect.left -= delta;
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if rect.left + delta + rect.right > focused_monitor_work_area.right {
|
||||
rect.left = focused_monitor_work_area.right - rect.right;
|
||||
} else {
|
||||
rect.left += delta;
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if rect.top - delta < focused_monitor_work_area.top {
|
||||
rect.top = focused_monitor_work_area.top;
|
||||
} else {
|
||||
rect.top -= delta;
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if rect.top + delta + rect.bottom > focused_monitor_work_area.bottom {
|
||||
rect.top = focused_monitor_work_area.bottom - rect.bottom;
|
||||
} else {
|
||||
rect.top += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::position_window(window.hwnd, &rect, false)?;
|
||||
if mouse_follows_focus {
|
||||
WindowsApi::center_cursor_in_rect(&rect)?;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn move_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
@@ -2125,6 +2386,54 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_floating_window_in_cycle_direction(
|
||||
&mut self,
|
||||
direction: CycleDirection,
|
||||
) -> Result<()> {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
|
||||
let mut target_idx = None;
|
||||
let len = focused_workspace.floating_windows().len();
|
||||
|
||||
if len > 1 {
|
||||
let focused_hwnd = WindowsApi::foreground_window()?;
|
||||
for (idx, window) in focused_workspace.floating_windows().iter().enumerate() {
|
||||
if window.hwnd == focused_hwnd {
|
||||
match direction {
|
||||
CycleDirection::Previous => {
|
||||
if idx == 0 {
|
||||
target_idx = Some(len - 1)
|
||||
} else {
|
||||
target_idx = Some(idx - 1)
|
||||
}
|
||||
}
|
||||
CycleDirection::Next => {
|
||||
if idx == len - 1 {
|
||||
target_idx = Some(0)
|
||||
} else {
|
||||
target_idx = Some(idx - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if target_idx.is_none() {
|
||||
target_idx = Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = target_idx {
|
||||
if let Some(window) = focused_workspace.floating_windows().get(idx) {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
@@ -3307,4 +3616,66 @@ impl WindowManager {
|
||||
.focused_window_mut()
|
||||
.ok_or_else(|| anyhow!("there is no window"))
|
||||
}
|
||||
|
||||
/// Updates the list of `known_hwnds` and their monitor/workspace index pair
|
||||
///
|
||||
/// [`known_hwnds`]: `Self.known_hwnds`
|
||||
pub fn update_known_hwnds(&mut self) {
|
||||
tracing::trace!("updating list of known hwnds");
|
||||
let mut known_hwnds = HashMap::new();
|
||||
for (m_idx, monitor) in self.monitors().iter().enumerate() {
|
||||
for (w_idx, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
for container in workspace.containers() {
|
||||
for window in container.windows() {
|
||||
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
|
||||
}
|
||||
|
||||
if let Some(window) = workspace.maximized_window() {
|
||||
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
for window in container.windows() {
|
||||
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.known_hwnds != known_hwnds {
|
||||
// Update reaper cache
|
||||
{
|
||||
let mut reaper_cache = crate::reaper::HWNDS_CACHE.lock();
|
||||
*reaper_cache = known_hwnds.clone();
|
||||
}
|
||||
|
||||
// Save to file
|
||||
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||
match OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(hwnd_json)
|
||||
{
|
||||
Ok(file) => {
|
||||
if let Err(error) =
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds.keys().collect::<Vec<_>>())
|
||||
{
|
||||
tracing::error!("Failed to save list of known_hwnds on file: {}", error);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to save list of known_hwnds on file: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Store new hwnds
|
||||
self.known_hwnds = known_hwnds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::window::should_act;
|
||||
use crate::window::Window;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -176,6 +177,8 @@ impl WindowManagerEvent {
|
||||
// [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange
|
||||
|
||||
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
let object_name_change_title_ignore_list =
|
||||
OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let title = &window.title().ok()?;
|
||||
@@ -183,7 +186,7 @@ impl WindowManagerEvent {
|
||||
let class = &window.class().ok()?;
|
||||
let path = &window.path().ok()?;
|
||||
|
||||
let should_trigger_show = should_act(
|
||||
let mut should_trigger_show = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
@@ -193,6 +196,14 @@ impl WindowManagerEvent {
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_trigger_show {
|
||||
for r in &*object_name_change_title_ignore_list {
|
||||
if r.is_match(title) {
|
||||
should_trigger_show = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// should not trigger show on minimized windows, for example when firefox sends
|
||||
// this message due to youtube autoplay changing the window title
|
||||
// https://github.com/LGUG2Z/komorebi/issues/941
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use core::ffi::c_void;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::convert::TryFrom;
|
||||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
@@ -11,7 +12,6 @@ use windows::core::Result as WindowsCrateResult;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HANDLE;
|
||||
use windows::Win32::Foundation::HINSTANCE;
|
||||
@@ -48,6 +48,8 @@ use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
|
||||
use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
|
||||
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::System::Power::RegisterPowerSettingNotification;
|
||||
use windows::Win32::System::Power::HPOWERNOTIFY;
|
||||
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
|
||||
use windows::Win32::System::RemoteDesktop::WTSRegisterSessionNotification;
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
@@ -92,6 +94,7 @@ use windows::Win32::UI::WindowsAndMessaging::MoveWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RegisterDeviceNotificationW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
||||
@@ -101,11 +104,14 @@ use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HDEVNOTIFY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
|
||||
@@ -133,6 +139,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||
use windows_core::BOOL;
|
||||
|
||||
use crate::core::Rect;
|
||||
|
||||
@@ -143,6 +150,7 @@ use crate::ring::Ring;
|
||||
use crate::set_window_position::SetWindowPosition;
|
||||
use crate::windows_callbacks;
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
|
||||
@@ -228,16 +236,9 @@ impl WindowsApi {
|
||||
callback: MONITORENUMPROC,
|
||||
callback_data_address: isize,
|
||||
) -> Result<()> {
|
||||
unsafe {
|
||||
EnumDisplayMonitors(
|
||||
HDC(std::ptr::null_mut()),
|
||||
None,
|
||||
callback,
|
||||
LPARAM(callback_data_address),
|
||||
)
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
unsafe { EnumDisplayMonitors(None, None, callback, LPARAM(callback_data_address)) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
|
||||
@@ -252,7 +253,10 @@ impl WindowsApi {
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||
pub fn load_monitor_information(wm: &mut WindowManager) -> Result<()> {
|
||||
let monitors = &mut wm.monitors;
|
||||
let monitor_usr_idx_map = &mut wm.monitor_usr_idx_map;
|
||||
|
||||
'read: for display in win32_display_data::connected_displays_all().flatten() {
|
||||
let path = display.device_path.clone();
|
||||
|
||||
@@ -283,6 +287,7 @@ impl WindowsApi {
|
||||
name,
|
||||
device,
|
||||
device_id,
|
||||
display.serial_number_id,
|
||||
);
|
||||
|
||||
let mut index_preference = None;
|
||||
@@ -295,7 +300,8 @@ impl WindowsApi {
|
||||
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
for (index, id) in &*display_index_preferences {
|
||||
if id.eq(m.device_id()) {
|
||||
if m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || id.eq(m.device_id())
|
||||
{
|
||||
index_preference = Option::from(index);
|
||||
}
|
||||
}
|
||||
@@ -324,6 +330,40 @@ impl WindowsApi {
|
||||
.elements_mut()
|
||||
.retain(|m| m.name().ne("PLACEHOLDER"));
|
||||
|
||||
// Rebuild monitor index map
|
||||
*monitor_usr_idx_map = HashMap::new();
|
||||
let mut added_monitor_idxs = Vec::new();
|
||||
for (index, id) in &*DISPLAY_INDEX_PREFERENCES.lock() {
|
||||
if let Some(m_idx) = monitors.elements().iter().position(|m| {
|
||||
m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || m.device_id() == id
|
||||
}) {
|
||||
monitor_usr_idx_map.insert(*index, m_idx);
|
||||
added_monitor_idxs.push(m_idx);
|
||||
}
|
||||
}
|
||||
|
||||
let max_usr_idx = monitors
|
||||
.elements()
|
||||
.len()
|
||||
.max(monitor_usr_idx_map.keys().max().map_or(0, |v| *v));
|
||||
|
||||
let mut available_usr_idxs = (0..max_usr_idx)
|
||||
.filter(|i| !monitor_usr_idx_map.contains_key(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let not_added_monitor_idxs = (0..monitors.elements().len())
|
||||
.filter(|i| !added_monitor_idxs.contains(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for i in not_added_monitor_idxs {
|
||||
if let Some(next_usr_idx) = available_usr_idxs.first() {
|
||||
monitor_usr_idx_map.insert(*next_usr_idx, i);
|
||||
available_usr_idxs.remove(0);
|
||||
} else if let Some(idx) = monitor_usr_idx_map.keys().max() {
|
||||
monitor_usr_idx_map.insert(*idx, i);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -472,7 +512,7 @@ impl WindowsApi {
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
position,
|
||||
Option::from(position),
|
||||
layout.left,
|
||||
layout.top,
|
||||
layout.right,
|
||||
@@ -510,7 +550,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
fn post_message(hwnd: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> Result<()> {
|
||||
unsafe { PostMessageW(hwnd, message, wparam, lparam) }.process()
|
||||
unsafe { PostMessageW(Option::from(hwnd), message, wparam, lparam) }.process()
|
||||
}
|
||||
|
||||
pub fn close_window(hwnd: isize) -> Result<()> {
|
||||
@@ -553,7 +593,7 @@ impl WindowsApi {
|
||||
// Error ignored, as the operation is not always necessary.
|
||||
let _ = SetWindowPos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
HWND_TOP,
|
||||
Option::from(HWND_TOP),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -569,7 +609,7 @@ impl WindowsApi {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn top_window() -> Result<isize> {
|
||||
unsafe { GetTopWindow(HWND::default())? }.process()
|
||||
unsafe { GetTopWindow(None)? }.process()
|
||||
}
|
||||
|
||||
pub fn desktop_window() -> Result<isize> {
|
||||
@@ -885,7 +925,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn is_window(hwnd: isize) -> bool {
|
||||
unsafe { IsWindow(HWND(as_ptr!(hwnd))) }.into()
|
||||
unsafe { IsWindow(Option::from(HWND(as_ptr!(hwnd)))) }.into()
|
||||
}
|
||||
|
||||
pub fn is_window_visible(hwnd: isize) -> bool {
|
||||
@@ -936,6 +976,7 @@ impl WindowsApi {
|
||||
name,
|
||||
device,
|
||||
device_id,
|
||||
display.serial_number_id,
|
||||
);
|
||||
|
||||
return Ok(monitor);
|
||||
@@ -1112,7 +1153,7 @@ impl WindowsApi {
|
||||
CW_USEDEFAULT,
|
||||
None,
|
||||
None,
|
||||
HINSTANCE(as_ptr!(instance)),
|
||||
Option::from(HINSTANCE(as_ptr!(instance))),
|
||||
Some(border as _),
|
||||
)?
|
||||
}
|
||||
@@ -1161,16 +1202,35 @@ impl WindowsApi {
|
||||
CW_USEDEFAULT,
|
||||
None,
|
||||
None,
|
||||
HINSTANCE(as_ptr!(instance)),
|
||||
Option::from(HINSTANCE(as_ptr!(instance))),
|
||||
None,
|
||||
)?
|
||||
}
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn register_power_setting_notification(
|
||||
hwnd: isize,
|
||||
guid: &windows_core::GUID,
|
||||
flags: REGISTER_NOTIFICATION_FLAGS,
|
||||
) -> WindowsCrateResult<HPOWERNOTIFY> {
|
||||
unsafe { RegisterPowerSettingNotification(HANDLE::from(HWND(as_ptr!(hwnd))), guid, flags) }
|
||||
}
|
||||
|
||||
pub fn register_device_notification(
|
||||
hwnd: isize,
|
||||
mut filter: DEV_BROADCAST_DEVICEINTERFACE_W,
|
||||
flags: REGISTER_NOTIFICATION_FLAGS,
|
||||
) -> WindowsCrateResult<HDEVNOTIFY> {
|
||||
unsafe {
|
||||
let state_ptr: *const c_void = &mut filter as *mut _ as *const c_void;
|
||||
RegisterDeviceNotificationW(HANDLE::from(HWND(as_ptr!(hwnd))), state_ptr, flags)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_rect(hwnd: isize, rect: Option<&Rect>, erase: bool) -> bool {
|
||||
let rect = rect.map(|rect| &rect.rect() as *const RECT);
|
||||
unsafe { InvalidateRect(HWND(as_ptr!(hwnd)), rect, erase) }.as_bool()
|
||||
unsafe { InvalidateRect(Option::from(HWND(as_ptr!(hwnd))), rect, erase) }.as_bool()
|
||||
}
|
||||
|
||||
pub fn alt_is_pressed() -> bool {
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::winevent_listener;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
@@ -21,6 +20,7 @@ use windows::Win32::UI::WindowsAndMessaging::OBJID_WINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows_core::BOOL;
|
||||
|
||||
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
|
||||
@@ -115,6 +115,16 @@ pub extern "system" fn win_event_hook(
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes the border focus state and colors don't get updated because this event comes too
|
||||
// slow for the value of GetForegroundWindow to be up to date by the time it is inspected in
|
||||
// the border manager to determine if a window show have its border show as "focused"
|
||||
//
|
||||
// so here we can just fire another event at the border manager when the system has finally
|
||||
// registered the new foreground window and this time the correct border colors will be applied
|
||||
if matches!(winevent, WinEvent::SystemForeground) && !has_filtered_style(hwnd) {
|
||||
border_manager::send_notification(Some(hwnd.0 as isize));
|
||||
}
|
||||
|
||||
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
|
||||
None => {
|
||||
tracing::trace!(
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::time::Duration;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::Accessibility::SetWinEventHook;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
@@ -41,7 +40,7 @@ pub fn start() {
|
||||
|
||||
loop {
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND(std::ptr::null_mut()), 0, 0).as_bool() {
|
||||
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
tracing::debug!("windows event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
@@ -54,42 +56,65 @@ use crate::REMOVE_TITLEBARS;
|
||||
)]
|
||||
pub struct Workspace {
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
name: Option<String>,
|
||||
containers: Ring<Container>,
|
||||
pub name: Option<String>,
|
||||
pub containers: Ring<Container>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
monocle_container: Option<Container>,
|
||||
pub monocle_container: Option<Container>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
monocle_container_restore_idx: Option<usize>,
|
||||
pub monocle_container_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
maximized_window: Option<Window>,
|
||||
pub maximized_window: Option<Window>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
maximized_window_restore_idx: Option<usize>,
|
||||
pub maximized_window_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
floating_windows: Vec<Window>,
|
||||
pub floating_windows: Vec<Window>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
layout: Layout,
|
||||
pub layout: Layout,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
layout_rules: Vec<(usize, Layout)>,
|
||||
pub layout_rules: Vec<(usize, Layout)>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
layout_flip: Option<Axis>,
|
||||
pub layout_flip: Option<Axis>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
workspace_padding: Option<i32>,
|
||||
pub workspace_padding: Option<i32>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
container_padding: Option<i32>,
|
||||
pub container_padding: Option<i32>,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
latest_layout: Vec<Rect>,
|
||||
pub latest_layout: Vec<Rect>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
resize_dimensions: Vec<Option<Rect>>,
|
||||
pub resize_dimensions: Vec<Option<Rect>>,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
tile: bool,
|
||||
pub tile: bool,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
apply_window_based_work_area_offset: bool,
|
||||
pub apply_window_based_work_area_offset: bool,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
float_override: Option<bool>,
|
||||
pub window_container_behaviour_rules: Option<Vec<(usize, WindowContainerBehaviour)>>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub float_override: Option<bool>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub layer: WorkspaceLayer,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub workspace_config: Option<WorkspaceConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub enum WorkspaceLayer {
|
||||
#[default]
|
||||
Tiling,
|
||||
Floating,
|
||||
}
|
||||
|
||||
impl Display for WorkspaceLayer {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
WorkspaceLayer::Tiling => write!(f, "Tiling"),
|
||||
WorkspaceLayer::Floating => write!(f, "Floating"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_ring_elements!(Workspace, Container);
|
||||
@@ -114,7 +139,10 @@ impl Default for Workspace {
|
||||
tile: true,
|
||||
apply_window_based_work_area_offset: true,
|
||||
window_container_behaviour: None,
|
||||
window_container_behaviour_rules: None,
|
||||
float_override: None,
|
||||
workspace_config: None,
|
||||
layer: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,42 +186,55 @@ impl Workspace {
|
||||
self.tile = false;
|
||||
}
|
||||
|
||||
let mut all_layout_rules = vec![];
|
||||
if let Some(layout_rules) = &config.layout_rules {
|
||||
let mut all_rules = vec![];
|
||||
for (count, rule) in layout_rules {
|
||||
all_rules.push((*count, Layout::Default(*rule)));
|
||||
all_layout_rules.push((*count, Layout::Default(*rule)));
|
||||
}
|
||||
|
||||
self.set_layout_rules(all_rules);
|
||||
|
||||
all_layout_rules.sort_by_key(|(i, _)| *i);
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
self.set_layout_rules(all_layout_rules.clone());
|
||||
|
||||
if let Some(layout_rules) = &config.custom_layout_rules {
|
||||
let rules = self.layout_rules_mut();
|
||||
for (count, pathbuf) in layout_rules {
|
||||
let rule = CustomLayout::from_path(pathbuf)?;
|
||||
rules.push((*count, Layout::Custom(rule)));
|
||||
all_layout_rules.push((*count, Layout::Custom(rule)));
|
||||
}
|
||||
|
||||
all_layout_rules.sort_by_key(|(i, _)| *i);
|
||||
self.tile = true;
|
||||
self.set_layout_rules(all_layout_rules);
|
||||
}
|
||||
|
||||
self.set_apply_window_based_work_area_offset(
|
||||
config.apply_window_based_work_area_offset.unwrap_or(true),
|
||||
);
|
||||
|
||||
if config.window_container_behaviour.is_some() {
|
||||
self.set_window_container_behaviour(config.window_container_behaviour);
|
||||
self.set_window_container_behaviour(config.window_container_behaviour);
|
||||
|
||||
if let Some(window_container_behaviour_rules) = &config.window_container_behaviour_rules {
|
||||
if window_container_behaviour_rules.is_empty() {
|
||||
self.set_window_container_behaviour_rules(None);
|
||||
} else {
|
||||
let mut all_rules = vec![];
|
||||
for (count, behaviour) in window_container_behaviour_rules {
|
||||
all_rules.push((*count, *behaviour));
|
||||
}
|
||||
|
||||
all_rules.sort_by_key(|(i, _)| *i);
|
||||
self.set_window_container_behaviour_rules(Some(all_rules));
|
||||
}
|
||||
} else {
|
||||
self.set_window_container_behaviour_rules(None);
|
||||
}
|
||||
|
||||
if config.float_override.is_some() {
|
||||
self.set_float_override(config.float_override);
|
||||
}
|
||||
self.set_float_override(config.float_override);
|
||||
self.set_layout_flip(config.layout_flip);
|
||||
|
||||
if config.layout_flip.is_some() {
|
||||
self.set_layout_flip(config.layout_flip);
|
||||
}
|
||||
self.set_workspace_config(Some(config.clone()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -326,21 +367,28 @@ impl Workspace {
|
||||
if !self.layout_rules().is_empty() {
|
||||
let mut updated_layout = None;
|
||||
|
||||
for rule in self.layout_rules() {
|
||||
if self.containers().len() >= rule.0 {
|
||||
updated_layout = Option::from(rule.1.clone());
|
||||
for (threshold, layout) in self.layout_rules() {
|
||||
if self.containers().len() >= *threshold {
|
||||
updated_layout = Option::from(layout.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(updated_layout) = updated_layout {
|
||||
if !matches!(updated_layout, Layout::Default(DefaultLayout::BSP)) {
|
||||
self.set_layout_flip(None);
|
||||
}
|
||||
|
||||
self.set_layout(updated_layout);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window_container_behaviour_rules) = self.window_container_behaviour_rules() {
|
||||
let mut updated_behaviour = None;
|
||||
for (threshold, behaviour) in window_container_behaviour_rules {
|
||||
if self.containers().len() >= *threshold {
|
||||
updated_behaviour = Option::from(*behaviour);
|
||||
}
|
||||
}
|
||||
|
||||
self.set_window_container_behaviour(updated_behaviour);
|
||||
}
|
||||
|
||||
let managed_maximized_window = self.maximized_window().is_some();
|
||||
|
||||
if *self.tile() {
|
||||
@@ -437,7 +485,16 @@ impl Workspace {
|
||||
// number of layouts / containers. This should never actually truncate as the remove_window
|
||||
// function takes care of cleaning up resize dimensions when destroying empty containers
|
||||
let container_count = self.containers().len();
|
||||
self.resize_dimensions_mut().resize(container_count, None);
|
||||
|
||||
// since monocle is a toggle, we never want to truncate the resize dimensions since it will
|
||||
// almost always be toggled off and the container will be reintegrated into layout
|
||||
//
|
||||
// without this check, if there are exactly two containers, when one is toggled to monocle
|
||||
// the resize dimensions will be truncated to len == 1, and when it is reintegrated, if it
|
||||
// had a resize adjustment before, that will have been lost
|
||||
if self.monocle_container().is_none() {
|
||||
self.resize_dimensions_mut().resize(container_count, None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -469,7 +526,15 @@ impl Workspace {
|
||||
}
|
||||
|
||||
for window in self.visible_windows().into_iter().flatten() {
|
||||
if !window.is_window() {
|
||||
if !window.is_window()
|
||||
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
|
||||
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
|
||||
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
|
||||
// docs is closed
|
||||
//
|
||||
// I hate every single person who worked on Microsoft Office 365, especially Word
|
||||
|| !window.is_visible()
|
||||
{
|
||||
hwnds.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
29
komorebi/tests/compat.rs
Normal file
29
komorebi/tests/compat.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use komorebi::StaticConfig;
|
||||
|
||||
#[test]
|
||||
fn backwards_compat() {
|
||||
let root = vec!["0.1.17", "0.1.18", "0.1.19"];
|
||||
let docs = vec![
|
||||
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27", "0.1.28",
|
||||
"0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33",
|
||||
];
|
||||
|
||||
let mut versions = vec![];
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
for version in root {
|
||||
let request = client.get(format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v{version}/komorebi.example.json")).header("User-Agent", "komorebi-backwards-compat-test").build().unwrap();
|
||||
versions.push((version, client.execute(request).unwrap().text().unwrap()));
|
||||
}
|
||||
|
||||
for version in docs {
|
||||
let request = client.get(format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v{version}/docs/komorebi.example.json")).header("User-Agent", "komorebi-backwards-compat-test").build().unwrap();
|
||||
versions.push((version, client.execute(request).unwrap().text().unwrap()));
|
||||
}
|
||||
|
||||
for (version, config) in versions {
|
||||
println!("{version}");
|
||||
StaticConfig::read_raw(&config).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
@@ -24,13 +24,10 @@ reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
shadow-rs = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
thiserror = "2"
|
||||
uds_windows = { workspace = true }
|
||||
which = { workspace = true }
|
||||
win32-display-data = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -1108,6 +1108,8 @@ enum SubCommand {
|
||||
/// Focus the specified monitor
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusMonitor(FocusMonitor),
|
||||
/// Focus the monitor at the current cursor location
|
||||
FocusMonitorAtCursor,
|
||||
/// Focus the last focused workspace on the focused monitor
|
||||
FocusLastWorkspace,
|
||||
/// Focus the specified workspace on the focused monitor
|
||||
@@ -1153,6 +1155,8 @@ enum SubCommand {
|
||||
/// Set offsets for a monitor to exclude parts of the work area from tiling
|
||||
#[clap(arg_required_else_help = true)]
|
||||
MonitorWorkAreaOffset(MonitorWorkAreaOffset),
|
||||
/// Toggle application of the window-based work area offset for the focused workspace
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
/// Set container padding on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusedWorkspaceContainerPadding(FocusedWorkspaceContainerPadding),
|
||||
@@ -1175,7 +1179,7 @@ enum SubCommand {
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
LoadCustomLayout(LoadCustomLayout),
|
||||
/// Flip the layout on the focused workspace (BSP only)
|
||||
/// Flip the layout on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FlipLayout(FlipLayout),
|
||||
/// Promote the focused window to the top of the tree
|
||||
@@ -1265,6 +1269,8 @@ enum SubCommand {
|
||||
/// 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.
|
||||
ToggleWorkspaceFloatOverride,
|
||||
/// Toggle between the Tiling and Floating layers on the focused workspace
|
||||
ToggleWorkspaceLayer,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
TogglePause,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
@@ -1876,6 +1882,9 @@ fn main() -> Result<()> {
|
||||
bottom: arg.bottom,
|
||||
}))?;
|
||||
}
|
||||
SubCommand::ToggleWindowBasedWorkAreaOffset => {
|
||||
send_message(&SocketMessage::ToggleWindowBasedWorkAreaOffset)?;
|
||||
}
|
||||
SubCommand::ContainerPadding(arg) => {
|
||||
send_message(&SocketMessage::ContainerPadding(
|
||||
arg.monitor,
|
||||
@@ -2588,6 +2597,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
SubCommand::FocusMonitor(arg) => {
|
||||
send_message(&SocketMessage::FocusMonitorNumber(arg.target))?;
|
||||
}
|
||||
SubCommand::FocusMonitorAtCursor => {
|
||||
send_message(&SocketMessage::FocusMonitorAtCursor)?;
|
||||
}
|
||||
SubCommand::FocusLastWorkspace => {
|
||||
send_message(&SocketMessage::FocusLastWorkspace)?;
|
||||
}
|
||||
@@ -2844,6 +2856,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
SubCommand::ToggleWorkspaceFloatOverride => {
|
||||
send_message(&SocketMessage::ToggleWorkspaceFloatOverride)?;
|
||||
}
|
||||
SubCommand::ToggleWorkspaceLayer => {
|
||||
send_message(&SocketMessage::ToggleWorkspaceLayer)?;
|
||||
}
|
||||
SubCommand::WindowHidingBehaviour(arg) => {
|
||||
send_message(&SocketMessage::WindowHidingBehaviour(arg.hiding_behaviour))?;
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ nav:
|
||||
- cli/send-to-monitor-workspace.md
|
||||
- cli/move-to-monitor-workspace.md
|
||||
- cli/focus-monitor.md
|
||||
- cli/focus-monitor-at-cursor.md
|
||||
- cli/focus-last-workspace.md
|
||||
- cli/focus-workspace.md
|
||||
- cli/focus-workspaces.md
|
||||
@@ -143,6 +144,7 @@ nav:
|
||||
- cli/invisible-borders.md
|
||||
- cli/global-work-area-offset.md
|
||||
- cli/monitor-work-area-offset.md
|
||||
- cli/toggle-window-based-work-area-offset.md
|
||||
- cli/focused-workspace-container-padding.md
|
||||
- cli/focused-workspace-padding.md
|
||||
- cli/adjust-container-padding.md
|
||||
@@ -175,6 +177,7 @@ nav:
|
||||
- cli/toggle-float-override.md
|
||||
- cli/toggle-workspace-window-container-behaviour.md
|
||||
- cli/toggle-workspace-float-override.md
|
||||
- cli/toggle-workspace-layer.md
|
||||
- cli/toggle-pause.md
|
||||
- cli/toggle-tiling.md
|
||||
- cli/toggle-float.md
|
||||
|
||||
657
schema.bar.json
657
schema.bar.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KomobarConfig",
|
||||
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.33`",
|
||||
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.35`",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"left_widgets",
|
||||
@@ -198,6 +198,38 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Custom format with modifiers",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"CustomModifiers"
|
||||
],
|
||||
"properties": {
|
||||
"CustomModifiers": {
|
||||
"description": "Custom format with additive modifiers for integer format specifiers",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"format",
|
||||
"modifiers"
|
||||
],
|
||||
"properties": {
|
||||
"format": {
|
||||
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
|
||||
"type": "string"
|
||||
},
|
||||
"modifiers": {
|
||||
"description": "Additive modifiers for integer format specifiers (e.g. { \"%U\": 1 } to increment the zero-indexed week number by 1)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -239,6 +271,66 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Keyboard"
|
||||
],
|
||||
"properties": {
|
||||
"Keyboard": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"data_refresh_interval": {
|
||||
"description": "Data refresh interval (default: 1 second)",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"enable": {
|
||||
"description": "Enable the Input widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -412,6 +504,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace_layer": {
|
||||
"description": "Configure the Workspace Layer widget",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"enable": {
|
||||
"description": "Enable the Komorebi Workspace Layer widget",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaces": {
|
||||
"description": "Configure the Workspaces widget",
|
||||
"type": "object",
|
||||
@@ -723,6 +828,13 @@
|
||||
"TwelveHour"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twelve-hour format (without seconds)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"TwelveHourWithoutSeconds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format (with seconds)",
|
||||
"type": "string",
|
||||
@@ -730,6 +842,27 @@
|
||||
"TwentyFourHour"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format (without seconds)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"TwentyFourHourWithoutSeconds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BinaryCircle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BinaryRectangle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
|
||||
"type": "object",
|
||||
@@ -1186,6 +1319,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
"description": "Bar height (default: 50)",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"icon_scale": {
|
||||
"description": "Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)",
|
||||
"type": "number",
|
||||
@@ -1380,6 +1518,38 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Custom format with modifiers",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"CustomModifiers"
|
||||
],
|
||||
"properties": {
|
||||
"CustomModifiers": {
|
||||
"description": "Custom format with additive modifiers for integer format specifiers",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"format",
|
||||
"modifiers"
|
||||
],
|
||||
"properties": {
|
||||
"format": {
|
||||
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
|
||||
"type": "string"
|
||||
},
|
||||
"modifiers": {
|
||||
"description": "Additive modifiers for integer format specifiers (e.g. { \"%U\": 1 } to increment the zero-indexed week number by 1)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1421,6 +1591,66 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Keyboard"
|
||||
],
|
||||
"properties": {
|
||||
"Keyboard": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"data_refresh_interval": {
|
||||
"description": "Data refresh interval (default: 1 second)",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"enable": {
|
||||
"description": "Enable the Input widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -1594,6 +1824,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace_layer": {
|
||||
"description": "Configure the Workspace Layer widget",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"enable": {
|
||||
"description": "Enable the Komorebi Workspace Layer widget",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaces": {
|
||||
"description": "Configure the Workspaces widget",
|
||||
"type": "object",
|
||||
@@ -1905,6 +2148,13 @@
|
||||
"TwelveHour"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twelve-hour format (without seconds)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"TwelveHourWithoutSeconds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format (with seconds)",
|
||||
"type": "string",
|
||||
@@ -1912,6 +2162,27 @@
|
||||
"TwentyFourHour"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format (without seconds)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"TwentyFourHourWithoutSeconds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BinaryCircle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BinaryRectangle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
|
||||
"type": "object",
|
||||
@@ -2028,26 +2299,14 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"max_label_width": {
|
||||
"description": "Max label width before text truncation (default: 400.0)",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"monitor": {
|
||||
"description": "Monitor options",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"index": {
|
||||
"description": "Komorebi monitor index of the monitor on which to render the bar",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
"margin": {
|
||||
"description": "Bar margin. Use one value for all sides or use a grouped margin for horizontal and/or vertical definition which can each take a single value for a symmetric margin or two values for each side, i.e.: ```json \"margin\": { \"horizontal\": 10 } ``` or: ```json \"margin\": { \"vertical\": [top, bottom] } ``` You can also set individual margin on each side like this: ```json \"margin\": { \"top\": 10, \"bottom\": 10, \"left\": 10, \"right\": 10, } ``` By default, margin is set to 0 on all sides.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"work_area_offset": {
|
||||
"description": "Automatically apply a work area offset for this monitor to accommodate the bar",
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bottom",
|
||||
@@ -2057,28 +2316,225 @@
|
||||
],
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"description": "The bottom point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"left": {
|
||||
"description": "The left point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"top": {
|
||||
"description": "The top point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"horizontal": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"vertical": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"max_label_width": {
|
||||
"description": "Max label width before text truncation (default: 400.0)",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"monitor": {
|
||||
"description": "The monitor index or the full monitor options",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "The monitor index where you want the bar to show",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"description": "The full monitor options with the index and an optional work_area_offset",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"index": {
|
||||
"description": "Komorebi monitor index of the monitor on which to render the bar",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"work_area_offset": {
|
||||
"description": "Automatically apply a work area offset for this monitor to accommodate the bar",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"top"
|
||||
],
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"description": "The bottom point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"left": {
|
||||
"description": "The left point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"top": {
|
||||
"description": "The top point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"padding": {
|
||||
"description": "Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or vertical definition which can each take a single value for a symmetric padding or two values for each side, i.e.: ```json \"padding\": { \"horizontal\": 10 } ``` or: ```json \"padding\": { \"horizontal\": [left, right] } ``` You can also set individual padding on each side like this: ```json \"padding\": { \"top\": 10, \"bottom\": 10, \"left\": 10, \"right\": 10, } ``` By default, padding is set to 10 on all sides.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"top"
|
||||
],
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"left": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"right": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"top": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"horizontal": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"vertical": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"position": {
|
||||
"description": "Bar positioning options",
|
||||
@@ -2315,6 +2771,38 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Custom format with modifiers",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"CustomModifiers"
|
||||
],
|
||||
"properties": {
|
||||
"CustomModifiers": {
|
||||
"description": "Custom format with additive modifiers for integer format specifiers",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"format",
|
||||
"modifiers"
|
||||
],
|
||||
"properties": {
|
||||
"format": {
|
||||
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
|
||||
"type": "string"
|
||||
},
|
||||
"modifiers": {
|
||||
"description": "Additive modifiers for integer format specifiers (e.g. { \"%U\": 1 } to increment the zero-indexed week number by 1)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2356,6 +2844,66 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Keyboard"
|
||||
],
|
||||
"properties": {
|
||||
"Keyboard": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"data_refresh_interval": {
|
||||
"description": "Data refresh interval (default: 1 second)",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"enable": {
|
||||
"description": "Enable the Input widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2529,6 +3077,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace_layer": {
|
||||
"description": "Configure the Workspace Layer widget",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"enable": {
|
||||
"description": "Enable the Komorebi Workspace Layer widget",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaces": {
|
||||
"description": "Configure the Workspaces widget",
|
||||
"type": "object",
|
||||
@@ -2840,6 +3401,13 @@
|
||||
"TwelveHour"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twelve-hour format (without seconds)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"TwelveHourWithoutSeconds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format (with seconds)",
|
||||
"type": "string",
|
||||
@@ -2847,6 +3415,27 @@
|
||||
"TwentyFourHour"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format (without seconds)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"TwentyFourHourWithoutSeconds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BinaryCircle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BinaryRectangle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
|
||||
"type": "object",
|
||||
@@ -3053,7 +3642,7 @@
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)",
|
||||
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"3024",
|
||||
|
||||
82
schema.json
82
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.33`",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.35`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
@@ -642,6 +642,53 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"floating_window_aspect_ratio": {
|
||||
"description": "Aspect ratio to resize with when toggling floating mode for a window",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A predefined aspect ratio",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "21:9",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Ultrawide"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "16:9",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Widescreen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "4:3",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Standard"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A custom W:H aspect ratio",
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"focus_follows_mouse": {
|
||||
"description": "END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead",
|
||||
"oneOf": [
|
||||
@@ -1237,7 +1284,7 @@
|
||||
]
|
||||
},
|
||||
"layout_rules": {
|
||||
"description": "Layout rules (default: None)",
|
||||
"description": "Layout rules in the format of threshold => layout (default: None)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
@@ -1276,6 +1323,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"window_container_behaviour_rules": {
|
||||
"description": "Window container behaviour rules in the format of threshold => behaviour (default: None)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Create a new container for each new window",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Create"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Append new windows to the focused window container",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Append"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"workspace_padding": {
|
||||
"description": "Container padding (default: global)",
|
||||
"type": "integer",
|
||||
@@ -1457,6 +1526,13 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"object_name_change_title_ignore_list": {
|
||||
"description": "Do not process EVENT_OBJECT_NAMECHANGE events as Show events for identified applications matching these title regexes",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"remove_titlebar_applications": {
|
||||
"description": "HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars",
|
||||
"type": "array",
|
||||
@@ -2191,7 +2267,7 @@
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)",
|
||||
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"3024",
|
||||
|
||||
Reference in New Issue
Block a user