mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-14 06:13:36 +01:00
Compare commits
281 Commits
feature/ta
...
feature/dr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f5fead85e | ||
|
|
6db73151f7 | ||
|
|
2d6ff0708f | ||
|
|
13a519fb29 | ||
|
|
9f8e4b9dca | ||
|
|
5a0196ac9d | ||
|
|
46d0e340f9 | ||
|
|
371ef88ecb | ||
|
|
f5b5070436 | ||
|
|
c8320552b0 | ||
|
|
2a5a960c34 | ||
|
|
10ab43a8ae | ||
|
|
0e8ed8aa40 | ||
|
|
fa2ccad5bf | ||
|
|
3c4ccd2504 | ||
|
|
7d821cd3db | ||
|
|
f4bbee0a2e | ||
|
|
2934d011fd | ||
|
|
71762a5961 | ||
|
|
76aeefa9f7 | ||
|
|
4968b0fe37 | ||
|
|
b4b400b236 | ||
|
|
2ee0bbc0c7 | ||
|
|
d38d3c956d | ||
|
|
052eb1c763 | ||
|
|
58730b81b4 | ||
|
|
274ae43e8f | ||
|
|
2a30f09bbd | ||
|
|
8fd18048a4 | ||
|
|
5809735024 | ||
|
|
96fdbbd1fb | ||
|
|
de131e9ca5 | ||
|
|
07dba03255 | ||
|
|
acd53dec1b | ||
|
|
a98968d179 | ||
|
|
8a32219867 | ||
|
|
ce4b75cc3c | ||
|
|
e4226ce623 | ||
|
|
4bfd7febb4 | ||
|
|
5cc688dc6b | ||
|
|
d897890032 | ||
|
|
e702d93a8a | ||
|
|
a8c687d3d5 | ||
|
|
30fbc1ae73 | ||
|
|
cb60e91842 | ||
|
|
64d29d606a | ||
|
|
072a62c314 | ||
|
|
a95e6e9644 | ||
|
|
6ba19d3ea2 | ||
|
|
edf1943157 | ||
|
|
d0c847e5bc | ||
|
|
992bc2abfe | ||
|
|
fa07f2d2f8 | ||
|
|
cc4e204191 | ||
|
|
24791f0ce5 | ||
|
|
6b95bf95f9 | ||
|
|
c0e1e9366d | ||
|
|
532436fe1a | ||
|
|
532949409c | ||
|
|
ec4a5e6491 | ||
|
|
f6e99eaac1 | ||
|
|
a6cf801a6b | ||
|
|
83d11c6f0f | ||
|
|
1ba1c57ba0 | ||
|
|
9f16894a09 | ||
|
|
df9ae931cc | ||
|
|
be2af9fdcb | ||
|
|
c083484ef0 | ||
|
|
1804b21c4a | ||
|
|
b6bd191cf5 | ||
|
|
5919f88b38 | ||
|
|
ff2aa5e51a | ||
|
|
42c12d5ec3 | ||
|
|
f0ce8e8572 | ||
|
|
bdea4821c3 | ||
|
|
229aeb7ddc | ||
|
|
17cbdc8663 | ||
|
|
9f3d806f79 | ||
|
|
fe9a1416e7 | ||
|
|
3618beb366 | ||
|
|
a4de2ee841 | ||
|
|
60e1834b43 | ||
|
|
54323c4c6a | ||
|
|
6516c808ee | ||
|
|
894b6f3d96 | ||
|
|
7ccdff4986 | ||
|
|
c48e1db0ff | ||
|
|
ea9752d5e1 | ||
|
|
acf780767c | ||
|
|
8e588d0284 | ||
|
|
91ff9b8852 | ||
|
|
81a7951312 | ||
|
|
555308db5f | ||
|
|
c90769f5fa | ||
|
|
76002385ab | ||
|
|
f40e80cd61 | ||
|
|
e4f9d8af86 | ||
|
|
0c64432c25 | ||
|
|
fe20caa56a | ||
|
|
02a2796e7d | ||
|
|
a0eb025cec | ||
|
|
70a61376a8 | ||
|
|
1761919707 | ||
|
|
1325da4e81 | ||
|
|
0776ca1565 | ||
|
|
724b0b7692 | ||
|
|
b53de81754 | ||
|
|
a837fea40c | ||
|
|
dd577c0eb3 | ||
|
|
7d497c3e14 | ||
|
|
e6398c29f8 | ||
|
|
ca893140f5 | ||
|
|
b26910aa58 | ||
|
|
487c217497 | ||
|
|
4031fbf033 | ||
|
|
dadc40777f | ||
|
|
59544edb74 | ||
|
|
5e2c18cad3 | ||
|
|
d69dfeb715 | ||
|
|
3a208b577c | ||
|
|
20817b094d | ||
|
|
394709e356 | ||
|
|
990a339d4e | ||
|
|
f0222dd4ab | ||
|
|
974e5a2b20 | ||
|
|
2bbc269b9f | ||
|
|
13ee42276d | ||
|
|
3641ce6b42 | ||
|
|
9d41a293f6 | ||
|
|
1756983978 | ||
|
|
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 | ||
|
|
cc51f62c3a | ||
|
|
b1db417df5 | ||
|
|
996a556984 | ||
|
|
c71e61fb1e | ||
|
|
2d97ee101d | ||
|
|
a4f69238b7 | ||
|
|
96f7eb1d31 | ||
|
|
28cd4a8801 | ||
|
|
3aa92a1255 | ||
|
|
281980b010 | ||
|
|
c063302c91 | ||
|
|
ba52dc3378 | ||
|
|
44716fdc98 | ||
|
|
4b30cecba9 | ||
|
|
d45cd729e8 | ||
|
|
5a8f48c6b9 | ||
|
|
4b9d811499 | ||
|
|
d520a2bf74 | ||
|
|
7ef4fd81c0 | ||
|
|
083ab65077 | ||
|
|
e9bb6b43d6 | ||
|
|
79eda30f48 | ||
|
|
692da90890 | ||
|
|
4babf336ec | ||
|
|
53a83eedb5 | ||
|
|
e1bbd3c1f5 | ||
|
|
2c08fbe8f6 | ||
|
|
cced2a4433 | ||
|
|
d93b6fa1b3 | ||
|
|
99353b8064 | ||
|
|
c64a42bca5 | ||
|
|
5ab5ec4f3a | ||
|
|
ad08585faf | ||
|
|
eb8a988841 | ||
|
|
0e2a55b300 | ||
|
|
eda91dcd1d | ||
|
|
0c6317a27b | ||
|
|
5c81a8c9e2 | ||
|
|
a4128b7276 | ||
|
|
73a4df884c | ||
|
|
32a234317c | ||
|
|
0dc6780da6 | ||
|
|
f089d3e59b | ||
|
|
5dbf0f1b89 | ||
|
|
d393f8fe77 | ||
|
|
c3769e7881 | ||
|
|
3c0b12f9af | ||
|
|
804faef229 | ||
|
|
7bf1521363 | ||
|
|
b49e634b65 | ||
|
|
be0671be6d | ||
|
|
10539a4bab | ||
|
|
9463c75f12 | ||
|
|
c31c5dc69d | ||
|
|
6c07863b81 | ||
|
|
40c55dec39 | ||
|
|
5cc2d9d469 | ||
|
|
91b255280a | ||
|
|
9bd1073a83 | ||
|
|
53c1990442 | ||
|
|
9d6173ecbb | ||
|
|
830da89529 | ||
|
|
f59d7a51f1 | ||
|
|
1470c63cfe | ||
|
|
64382b18c1 | ||
|
|
26f90cc9ee | ||
|
|
192af6751b | ||
|
|
4f306e5bfd | ||
|
|
ede0b23bb4 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
labels: [ bug ]
|
||||
labels: [bug]
|
||||
title: "[BUG]: "
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -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'
|
||||
});
|
||||
13
.github/workflows/windows.yaml
vendored
13
.github/workflows/windows.yaml
vendored
@@ -18,6 +18,14 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -47,7 +55,8 @@ jobs:
|
||||
key: ${{ matrix.platform.target }}
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
- uses: houseabsolute/actions-rust-cross@v0
|
||||
- run: cargo test
|
||||
- uses: houseabsolute/actions-rust-cross@v1
|
||||
with:
|
||||
command: "build"
|
||||
target: ${{ matrix.platform.target }}
|
||||
@@ -199,7 +208,7 @@ jobs:
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
- uses: vedantmgoyal2009/winget-releaser@main
|
||||
with:
|
||||
identifier: LGUG2Z.komorebi
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
40
CODE_OF_CONDUCT.md
Normal file
40
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# The Komorebi Code of Conduct
|
||||
|
||||
This document is based on the [Rust Code of
|
||||
Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
|
||||
## Conduct
|
||||
|
||||
- We are committed to providing a friendly, safe and welcoming environment for
|
||||
all, regardless of level of experience, gender identity and expression, sexual
|
||||
orientation, disability, personal appearance, body size, race, ethnicity, age,
|
||||
religion, nationality, or other similar characteristic.
|
||||
|
||||
- Please avoid using overtly sexual aliases or other nicknames that might
|
||||
detract from a friendly, safe and welcoming environment for all.
|
||||
|
||||
- Please be kind and courteous. There’s no need to be mean or rude.
|
||||
|
||||
- Respect that people have differences of opinion and that every design or
|
||||
implementation choice carries a trade-off and numerous costs. There is seldom a
|
||||
right answer.
|
||||
|
||||
- Please keep unstructured critique to a minimum. If you have solid ideas you
|
||||
want to experiment with, make a fork and see how it works.
|
||||
|
||||
- We will exclude you from interaction if you insult, demean or harass anyone.
|
||||
That is not welcome behavior. We interpret the term “harassment” as including
|
||||
the definition in the [Citizen Code of
|
||||
Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md);
|
||||
if you have any lack of clarity about what might be included in that concept,
|
||||
please read their definition. In particular, we don’t tolerate behavior that
|
||||
excludes people in socially marginalized groups.
|
||||
|
||||
- Private harassment is also unacceptable. No matter who you are, if you feel
|
||||
you have been or are being harassed or made uncomfortable by a community member,
|
||||
please contact me immediately. Whether you’re a regular contributor or a
|
||||
newcomer, we care about making this community a safe place for you and we’ve got
|
||||
your back.
|
||||
|
||||
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing
|
||||
behavior is not welcome.
|
||||
2694
Cargo.lock
generated
2694
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
@@ -13,13 +13,14 @@ members = [
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
chrono-tz = "0.10"
|
||||
chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.29"
|
||||
egui_extras = "0.29"
|
||||
dirs = "5"
|
||||
eframe = "0.31"
|
||||
egui_extras = "0.31"
|
||||
dirs = "6"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
schemars = "0.8"
|
||||
@@ -27,33 +28,38 @@ lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
paste = "1"
|
||||
sysinfo = "0.31"
|
||||
sysinfo = "0.33"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
|
||||
windows-implement = { version = "0.58" }
|
||||
windows-interface = { version = "0.58" }
|
||||
windows-core = { version = "0.58" }
|
||||
shadow-rs = "0.35"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
|
||||
windows-numerics = { version = "0.2" }
|
||||
windows-implement = { version = "0.60" }
|
||||
windows-interface = { version = "0.59" }
|
||||
windows-core = { version = "0.61" }
|
||||
shadow-rs = "1"
|
||||
which = "7"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
version = "0.61"
|
||||
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",
|
||||
@@ -66,4 +72,4 @@ features = [
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
]
|
||||
]
|
||||
24
LICENSE.md
24
LICENSE.md
@@ -1,6 +1,6 @@
|
||||
# Komorebi License
|
||||
|
||||
Version 1.0.0
|
||||
Version 2.0.0
|
||||
|
||||
## Acceptance
|
||||
|
||||
@@ -13,9 +13,20 @@ your licenses.
|
||||
The licensor grants you a copyright license for the software
|
||||
to do everything you might do with the software that would
|
||||
otherwise infringe the licensor's copyright in it for any
|
||||
permitted purpose. However, you may only make changes according
|
||||
permitted purpose. However, you may only distribute the source
|
||||
code of the software according to the [Distribution License](
|
||||
#distribution-license), you may only make changes according
|
||||
to the [Changes License](#changes-license), and you may not
|
||||
distribute the software or new works based on the software.
|
||||
otherwise distribute the software or new works based on the
|
||||
software.
|
||||
|
||||
## Distribution License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
distribute copies of the source code of the software. Your
|
||||
license to distribute covers distributing the source code of
|
||||
the software with changes permitted by the [Changes License](
|
||||
#changes-license).
|
||||
|
||||
## Changes License
|
||||
|
||||
@@ -45,7 +56,7 @@ law. These terms do not limit them.
|
||||
|
||||
These terms do not allow you to sublicense or transfer any of
|
||||
your licenses to anyone else, or prevent the licensor from
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
any other licenses.
|
||||
|
||||
## Patent Defense
|
||||
@@ -63,7 +74,7 @@ violated any of these terms, or done anything with the software
|
||||
not covered by your licenses, your licenses can nonetheless
|
||||
continue if you come into full compliance with these terms,
|
||||
and take practical steps to correct past violations, within
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
end immediately.
|
||||
|
||||
## No Liability
|
||||
@@ -88,11 +99,10 @@ organizations that have control over, are under the control of,
|
||||
or are under common control with that organization. **Control**
|
||||
means ownership of substantially all the assets of an entity,
|
||||
or the power to direct its management and policies by vote,
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
**Your licenses** are all the licenses granted to you for the
|
||||
software under these terms.
|
||||
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
||||
|
||||
|
||||
124
README.md
124
README.md
@@ -7,9 +7,9 @@ Tiling Window Management for Windows.
|
||||
<img alt="Tech for Palestine" src="https://badge.techforpalestine.org/default">
|
||||
</a>
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LGUG2Z/komorebi/.github/workflows/windows.yaml">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LGUG2Z/komorebi">
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/LGUG2Z/komorebi/total">
|
||||
<img alt="GitHub commits since latest release (by date) for a branch" src="https://img.shields.io/github/commits-since/LGUG2Z/komorebi/latest">
|
||||
<img alt="Active Individual Commercial Use Licenses" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Flgug2z-ecstaticmagentacheetah.web.val.run&query=%24.&label=active%20individual%20commercial%20use%20licenses&cacheSeconds=3600&link=https%3A%2F%2Flgug2z.com%2Fsoftware%2Fkomorebi">
|
||||
<a href="https://discord.gg/mGkn66PHkx">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/898554690126630914">
|
||||
</a>
|
||||
@@ -29,6 +29,8 @@ Tiling Window Management for Windows.
|
||||
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
|
||||
[Desktop Window
|
||||
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
|
||||
@@ -50,6 +52,8 @@ _komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflow
|
||||
[configuration schema reference](https://komorebi.lgug2z.com/schema) and a
|
||||
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
|
||||
|
||||
## Community
|
||||
|
||||
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
|
||||
_komorebi_-related discussion, help, troubleshooting etc. If you have any
|
||||
specific feature requests or bugs to report, please create an issue in this
|
||||
@@ -57,28 +61,66 @@ repository.
|
||||
|
||||
There is a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
_komorebi_ development videos. If you would like to be notified of upcoming
|
||||
videos please subscribe and turn on notifications.
|
||||
_komorebi_ development videos, feature previews and release overviews. Subscribing
|
||||
to the channel (which is monetized as part of the YouTube Partner Program) and
|
||||
watching videos is a really simple and passive way to contribute financially to
|
||||
the development and maintenance of _komorebi_.
|
||||
|
||||
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
|
||||
showcases the many awesome projects that exist in the _komorebi_ ecosystem.
|
||||
|
||||
_komorebi_ is a free and source-available project, and one that encourages you to
|
||||
make charitable donations if you find the software to be useful and have the
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is [educational source
|
||||
software](https://lgug2z.com/articles/educational-source-software/).
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 2.0.0
|
||||
license](https://github.com/LGUG2Z/komorebi-license), which is a fork of the
|
||||
[PolyForm Strict 1.0.0
|
||||
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
|
||||
this means that you are free to do whatever you want with `komorebi` for
|
||||
personal use other than redistribution, or distribution of new works (i.e.
|
||||
hard-forks) based on the software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either
|
||||
for personal use or for integration back upstream via pull requests.
|
||||
|
||||
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
not permit any kind of commercial use (i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
|
||||
_komorebi_ is a free and educational source project, and one that encourages you
|
||||
to make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
I encourage you to make a charitable donation to the [Palestine Children's
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or contributing
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
|
||||
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
|
||||
me on GitHub.
|
||||
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have
|
||||
anything specific to offer besides my gratitude and shout outs at the end of
|
||||
_komorebi_ live development videos and tutorials.
|
||||
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
|
||||
the Discord server, get shout outs at the end of _komorebi_-related videos on
|
||||
YouTube, gain the ability to submit feature requests on the issue tracker, and
|
||||
receive releases of komorebi with "easter eggs" on physical media.
|
||||
|
||||
If you would like to tip or sponsor the project but are unable to use GitHub
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z).
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
|
||||
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
|
||||
|
||||
## Licensing for Commercial Use
|
||||
|
||||
A dedicated Individual Commercial Use License is available for those who want to
|
||||
use `komorebi` at work.
|
||||
|
||||
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
|
||||
for the licensed individual only, for the duration of a valid paid license
|
||||
subscription only. All provisions and restrictions enumerated in the [Komorebi
|
||||
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
|
||||
|
||||
More information, pricing and purchase links for Individual Commercial Use
|
||||
Licenses [can be found here](https://lgug2z.com/software/komorebi).
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -104,7 +146,8 @@ video will answer the majority of your questions.
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
|
||||
unfocused window transparency and animations enabled, using a custom status bar integrated using
|
||||
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
_komorebi_'
|
||||
s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
|
||||
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
|
||||
|
||||
@@ -125,7 +168,11 @@ https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-
|
||||
|
||||
# Contribution Guidelines
|
||||
|
||||
If you would like to contribute to `komorebi` please take the time to carefully read the guidelines below.
|
||||
If you would like to contribute to `komorebi` please take the time to carefully
|
||||
read the guidelines below.
|
||||
|
||||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
|
||||
code contributions to `komorebi` are licensed.
|
||||
|
||||
## Commit hygiene
|
||||
|
||||
@@ -135,8 +182,8 @@ If you would like to contribute to `komorebi` please take the time to carefully
|
||||
- Use `git cz` with
|
||||
the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare
|
||||
commit messages
|
||||
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the
|
||||
changes being committed
|
||||
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for
|
||||
the changes being committed
|
||||
|
||||
## PRs should contain only a single feature or bug fix
|
||||
|
||||
@@ -175,7 +222,8 @@ This includes but is not limited to:
|
||||
|
||||
- All `komorebic` commands
|
||||
- The `komorebi.json` schema
|
||||
- The [`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
- The [
|
||||
`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
schema
|
||||
|
||||
No user should ever find that their configuration file has stopped working after upgrading to a new version
|
||||
@@ -191,27 +239,6 @@ ability for users to specify colours in `komorebi.json` in Hex format alongside
|
||||
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
|
||||
required.
|
||||
|
||||
## License
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
|
||||
is a fork of the [PolyForm Strict 1.0.0
|
||||
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
|
||||
this means that you are free to do whatever you want with `komorebi` for
|
||||
personal use other than redistribution, or distribution of new works (ie.
|
||||
hard-forks) based on the software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended
|
||||
either for personal use or for integration back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](./LICENSE.md) does not permit any kind of
|
||||
commercial use.
|
||||
|
||||
A dedicated license and EULA will be introduced in 2025 for both commercial and
|
||||
noncommercial organizations.
|
||||
|
||||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
|
||||
code contributions to `komorebi` are licensed.
|
||||
|
||||
# Development
|
||||
|
||||
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
|
||||
@@ -220,13 +247,13 @@ the IDE for completions and navigation:
|
||||
- Set `Expand declarative macros`
|
||||
to `Use new engine` under "Settings > Langauges & Frameworks > Rust"
|
||||
- Enable the following experimental features:
|
||||
- `org.rust.cargo.evaluate.build.scripts`
|
||||
- `org.rust.macros.proc`
|
||||
- `org.rust.cargo.evaluate.build.scripts`
|
||||
- `org.rust.macros.proc`
|
||||
|
||||
# Logs and Debugging
|
||||
|
||||
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or overwritten, so it will keep
|
||||
growing until it is deleted by the user.
|
||||
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or
|
||||
overwritten, so it will keep growing until it is deleted by the user.
|
||||
|
||||
Whenever running the `komorebic stop` command or sending a Ctrl-C signal to `komorebi` directly, the `komorebi` process
|
||||
ensures that all hidden windows are restored before termination.
|
||||
@@ -367,7 +394,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.30"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.35"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
@@ -442,12 +469,17 @@ programming languages.
|
||||
|
||||
# Appreciations
|
||||
|
||||
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its never-ending development
|
||||
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its
|
||||
never-ending development
|
||||
|
||||
- Thank you to [@sitiom](https://github.com/sitiom) for being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
|
||||
- Thank you to [@sitiom](https://github.com/sitiom) for
|
||||
being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
|
||||
|
||||
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught me more than I can ever hope to repay
|
||||
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught
|
||||
me more than I can ever hope to repay
|
||||
|
||||
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling window management on Windows with me and having an excellent spirit of collaboration
|
||||
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling
|
||||
window management on Windows with me and having an excellent spirit of collaboration
|
||||
|
||||
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`
|
||||
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring
|
||||
the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`
|
||||
|
||||
99
deny.toml
Normal file
99
deny.toml
Normal file
@@ -0,0 +1,99 @@
|
||||
[graph]
|
||||
targets = [
|
||||
"x86_64-pc-windows-msvc",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc",
|
||||
]
|
||||
all-features = false
|
||||
no-default-features = false
|
||||
|
||||
[output]
|
||||
feature-depth = 1
|
||||
|
||||
[advisories]
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
|
||||
]
|
||||
|
||||
[licenses]
|
||||
allow = [
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"Artistic-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0",
|
||||
"OFL-1.1",
|
||||
"Ubuntu-font-1.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"LicenseRef-Komorebi-1.0"
|
||||
]
|
||||
confidence-threshold = 0.8
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-client"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic-no-console"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-themes"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-gui"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-bar"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "base16-egui-themes"
|
||||
expression = "MIT"
|
||||
license-files = []
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "allow"
|
||||
highlight = "all"
|
||||
workspace-default-features = "allow"
|
||||
external-default-features = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = [
|
||||
"https://github.com/LGUG2Z/base16-egui-themes",
|
||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||
"https://github.com/LGUG2Z/windows-icons",
|
||||
"https://github.com/LGUG2Z/win32-display-data",
|
||||
"https://github.com/LGUG2Z/flavours",
|
||||
"https://github.com/LGUG2Z/base16_color_scheme",
|
||||
]
|
||||
771
dependencies.json
Normal file
771
dependencies.json
Normal file
@@ -0,0 +1,771 @@
|
||||
{
|
||||
"licenses": [
|
||||
[
|
||||
"0BSD",
|
||||
[
|
||||
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"win32-display-data 0.1.0 git+https://github.com/LGUG2Z/win32-display-data?rev=55cebdebfbd68dbd14945a1ba90f6b05b7be2893"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Apache-2.0",
|
||||
[
|
||||
"ab_glyph 0.2.29 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ab_glyph_rasterizer 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit 0.17.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_consumer 0.26.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_windows 0.24.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_winit 0.23.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anyhow 1.0.97 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arboard 3.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bit_field 0.10.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitstream-io 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cc 1.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_derive 4.5.28 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crc32fast 1.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.4.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deranged 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"document-features 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dpi 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ecolor 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eframe 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-phosphor 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-winit 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_extras 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_glow 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"either 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"emath 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"filetime 0.2.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flate2 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gif 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gl_generator 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin 0.32.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin_egl_sys 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin_wgl_sys 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"half 2.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iana-time-zone 0.1.61 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.25.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image-webp 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"imgref 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"immutable-chunkmap 2.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jobserver 0.1.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jpeg-decoder 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"khronos_api 3.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.170 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libgit2-sys 0.18.0+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libz-sys 1.1.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litrs 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"log 0.4.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miette 7.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miette-derive 7.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miow 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nohash-hasher 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ntapi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"once_cell 1.20.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owned_ttf_parser 0.25.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"percent-encoding 2.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"png 0.17.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ppv-lite86 0.2.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling-procmacros 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex 1.11.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"reqwest 0.12.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustversion 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shadow-rs 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smallvec 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"socket2 0.5.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"supports-color 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"supports-hyperlinks 3.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"supports-unicode 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 2.0.99 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"sync_wrapper 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tempfile 3.17.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time 0.3.37 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time-core 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tzdb 0.7.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicase 2.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-linebreak 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"uom 0.36.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"url 2.5.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"weezl 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-link 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-registry 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winit 0.30.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"wmi 0.15.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Artistic-2.0",
|
||||
[
|
||||
"file-id 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"notify-debouncer-full 0.1.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"BSD-2-Clause",
|
||||
[
|
||||
"av1-grain 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rav1e 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"v_frame 0.3.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.7.35 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"BSD-3-Clause",
|
||||
[
|
||||
"alloc-no-stdlib 2.0.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"alloc-stdlib 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"avif-serialize 0.8.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli-decompressor 2.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"exr 1.73.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lebe 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ravif 0.11.11 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"BSL-1.0",
|
||||
[
|
||||
"clipboard-win 5.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"error-code 3.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"CC0-1.0",
|
||||
[
|
||||
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"file-id 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"imgref 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"notify 6.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"notify-debouncer-full 0.1.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"ISC",
|
||||
[
|
||||
"is_ci 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libloading 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"starship-battery 0.10.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"MIT",
|
||||
[
|
||||
"accesskit 0.17.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_consumer 0.26.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_windows 0.24.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"aho-corasick 1.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"aligned-vec 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anyhow 1.0.97 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arboard 3.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arg_enum_proc_macro 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bit_field 0.10.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitstream-io 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli-decompressor 2.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"built 0.7.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"byteorder-lite 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytes 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"catppuccin-egui 5.3.1 git+https://github.com/LGUG2Z/catppuccin-egui?rev=bdaff30959512c4f7ee7304117076a48633d777f",
|
||||
"cc 1.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg_aliases 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_derive 4.5.28 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color_quant 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crc32fast 1.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.4.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deranged 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"document-features 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ecolor 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eframe 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-phosphor 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-winit 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_extras 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_glow 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"either 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"emath 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"filetime 0.2.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flate2 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"font-loader 0.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fs-tail 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getset 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gif 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin-winit 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"h2 0.4.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"half 2.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http-body 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http-body-util 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper 1.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-util 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iana-time-zone 0.1.61 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.25.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image-webp 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"immutable-chunkmap 2.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jobserver 0.1.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jpeg-decoder 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.170 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libgit2-sys 0.18.0+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libz-sys 1.1.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litrs 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"log 0.4.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"loop9 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"matchers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"maybe-rayon 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"memchr 2.7.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"memoffset 0.9.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mime_guess2 2.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mio 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miow 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nanoid 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"netdev 0.32.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"new_debug_unreachable 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nohash-hasher 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nom 7.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"noop_proc_macro 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ntapi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nu-ansi-term 0.46.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"once_cell 1.20.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"os_info 3.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"overload 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owo-colors 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owo-colors 4.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"percent-encoding 2.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"png 0.17.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powershell_script 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ppv-lite86 0.2.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling-procmacros 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"random_word 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex 1.11.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-automata 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"reqwest 0.12.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rgb 0.8.50 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustversion 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"same-file 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"schannel 0.1.27 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"schemars 0.8.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"schemars_derive 0.8.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shadow-rs 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"sharded-slab 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"simd-adler32 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"simd_helpers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"slab 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smallvec 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"socket2 0.5.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"strsim 0.11.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"strum 0.27.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"strum_macros 0.27.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 2.0.99 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"synstructure 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"sysinfo 0.33.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tempfile 3.17.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"textwrap 0.16.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tiff 0.9.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time 0.3.37 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time-core 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio 1.43.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-native-tls 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-util 0.7.13 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tower 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tower-layer 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tower-service 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing 0.1.41 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-appender 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-attributes 0.1.28 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-core 0.1.33 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-error 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-log 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-subscriber 0.3.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"try-lock 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"uds_windows 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicase 2.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unsafe-libyaml 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"uom 0.36.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"url 2.5.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"want 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"weezl 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"which 7.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winapi-util 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-icons 0.1.0 git+https://github.com/LGUG2Z/windows-icons?rev=d67cc9920aa9b4883393e411fb4fa2ddd4c498b5",
|
||||
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-link 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-registry 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winput 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winreg 0.55.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winsafe 0.0.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"wmi 0.15.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"xml-rs 0.8.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"MIT-0",
|
||||
[
|
||||
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tzdb_data 0.2.1 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"MPL-2.0",
|
||||
[
|
||||
"option-ext 0.2.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"OFL-1.1",
|
||||
[
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Ubuntu-font-1.0",
|
||||
[
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Unicode-3.0",
|
||||
[
|
||||
"icu_collections 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locid 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locid_transform 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locid_transform_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties 1.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_provider 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_provider_macros 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litemap 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tinystr 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"writeable 0.5.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke-derive 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom-derive 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec-derive 0.10.3 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Unlicense",
|
||||
[
|
||||
"aho-corasick 1.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"byteorder-lite 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"memchr 2.7.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-automata 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"same-file 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winapi-util 0.1.9 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Zlib",
|
||||
[
|
||||
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"const_format 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"const_format_proc_macros 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"foldhash 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -3,13 +3,18 @@
|
||||
```
|
||||
Set the duration for movement animations in ms
|
||||
|
||||
Usage: komorebic.exe animation-duration <DURATION>
|
||||
Usage: komorebic.exe animation-duration [OPTIONS] <DURATION>
|
||||
|
||||
Arguments:
|
||||
<DURATION>
|
||||
Desired animation durations in ms
|
||||
|
||||
Options:
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the duration to. If not specified, sets global duration
|
||||
|
||||
[possible values: movement, transparency]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
@@ -10,8 +10,13 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo,
|
||||
ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
|
||||
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the style to. If not specified, sets global style
|
||||
|
||||
[possible values: movement, transparency]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
```
|
||||
Enable or disable movement animations
|
||||
|
||||
Usage: komorebic.exe animation <BOOLEAN_STATE>
|
||||
Usage: komorebic.exe animation [OPTIONS] <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the state to. If not specified, sets global state
|
||||
|
||||
[possible values: movement, transparency]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Arguments:
|
||||
Options:
|
||||
-w, --window-kind <WINDOW_KIND>
|
||||
[default: single]
|
||||
[possible values: single, stack, monocle, unfocused, floating]
|
||||
[possible values: single, stack, monocle, unfocused, unfocused-locked, floating]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
```
|
||||
Check komorebi configuration and related files for common errors
|
||||
|
||||
Usage: komorebic.exe check
|
||||
Usage: komorebic.exe check [OPTIONS]
|
||||
|
||||
Options:
|
||||
-k, --komorebi-config <KOMOREBI_CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
12
docs/cli/clear-session-float-rules.md
Normal file
12
docs/cli/clear-session-float-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# clear-session-float-rules
|
||||
|
||||
```
|
||||
Clear all session float rules
|
||||
|
||||
Usage: komorebic.exe clear-session-float-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/close-workspace.md
Normal file
12
docs/cli/close-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# close-workspace
|
||||
|
||||
```
|
||||
Close the focused workspace (must be empty and unnamed)
|
||||
|
||||
Usage: komorebic.exe close-workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/cycle-empty-workspace.md
Normal file
16
docs/cli/cycle-empty-workspace.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# cycle-empty-workspace
|
||||
|
||||
```
|
||||
Focus the next empty workspace in the given cycle direction (if one exists)
|
||||
|
||||
Usage: komorebic.exe cycle-empty-workspace <CYCLE_DIRECTION>
|
||||
|
||||
Arguments:
|
||||
<CYCLE_DIRECTION>
|
||||
[possible values: previous, next]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/cycle-stack-index.md
Normal file
16
docs/cli/cycle-stack-index.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# cycle-stack-index
|
||||
|
||||
```
|
||||
Cycle the index of the focused window in the focused stack in the specified cycle direction
|
||||
|
||||
Usage: komorebic.exe cycle-stack-index <CYCLE_DIRECTION>
|
||||
|
||||
Arguments:
|
||||
<CYCLE_DIRECTION>
|
||||
[possible values: previous, next]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/data-directory.md
Normal file
12
docs/cli/data-directory.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# data-directory
|
||||
|
||||
```
|
||||
Show the path to komorebi's data directory in %LOCALAPPDATA%
|
||||
|
||||
Usage: komorebic.exe data-directory
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/eager-focus.md
Normal file
16
docs/cli/eager-focus.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# eager-focus
|
||||
|
||||
```
|
||||
Focus the first managed window matching the given exe
|
||||
|
||||
Usage: komorebic.exe eager-focus <EXE>
|
||||
|
||||
Arguments:
|
||||
<EXE>
|
||||
Case-sensitive exe identifier
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -18,6 +18,9 @@ Options:
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
--masir
|
||||
Enable autostart of masir
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
12
docs/cli/enforce-workspace-rules.md
Normal file
12
docs/cli/enforce-workspace-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# enforce-workspace-rules
|
||||
|
||||
```
|
||||
Enforce all workspace rules, including initial workspace rules that have already been applied
|
||||
|
||||
Usage: komorebic.exe enforce-workspace-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
```
|
||||
24
docs/cli/kill.md
Normal file
24
docs/cli/kill.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# kill
|
||||
|
||||
```
|
||||
Kill background processes started by komorebic
|
||||
|
||||
Usage: komorebic.exe kill [OPTIONS]
|
||||
|
||||
Options:
|
||||
--whkd
|
||||
Kill whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Kill ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Kill komorebi-bar if it is running as a background process
|
||||
|
||||
--masir
|
||||
Kill masir if it is running as a background process
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/move-to-last-workspace.md
Normal file
12
docs/cli/move-to-last-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# move-to-last-workspace
|
||||
|
||||
```
|
||||
Move the focused window to the last focused monitor workspace
|
||||
|
||||
Usage: komorebic.exe move-to-last-workspace
|
||||
|
||||
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, focused-workspace-layout, version]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
12
docs/cli/send-to-last-workspace.md
Normal file
12
docs/cli/send-to-last-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# send-to-last-workspace
|
||||
|
||||
```
|
||||
Send the focused window to the last focused monitor workspace
|
||||
|
||||
Usage: komorebic.exe send-to-last-workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/session-float-rule.md
Normal file
12
docs/cli/session-float-rule.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# session-float-rule
|
||||
|
||||
```
|
||||
Add a rule to float the foreground window for the rest of this session
|
||||
|
||||
Usage: komorebic.exe session-float-rule
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/session-float-rules.md
Normal file
12
docs/cli/session-float-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# session-float-rules
|
||||
|
||||
```
|
||||
Show all session float rules
|
||||
|
||||
Usage: komorebic.exe session-float-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
18
docs/cli/stackbar-mode.md
Normal file
18
docs/cli/stackbar-mode.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# stackbar-mode
|
||||
|
||||
```
|
||||
Set the stackbar mode
|
||||
|
||||
Usage: komorebic.exe stackbar-mode <MODE>
|
||||
|
||||
Arguments:
|
||||
<MODE>
|
||||
Desired stackbar mode
|
||||
|
||||
[possible values: always, never, on-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -24,6 +24,12 @@ Options:
|
||||
--bar
|
||||
Start komorebi-bar in a background process
|
||||
|
||||
--masir
|
||||
Start masir in a background process for focus-follows-mouse
|
||||
|
||||
--clean-state
|
||||
Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ Options:
|
||||
--bar
|
||||
Stop komorebi-bar if it is running as a background process
|
||||
|
||||
--masir
|
||||
Stop masir if it is running as a background process
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
12
docs/cli/toggle-lock.md
Normal file
12
docs/cli/toggle-lock.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-lock
|
||||
|
||||
```
|
||||
Toggle a lock for the focused container, ensuring it will not be displaced by any new windows
|
||||
|
||||
Usage: komorebic.exe toggle-lock
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print 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)
|
||||
|
||||
412
docs/common-workflows/multi-monitor-setup.md
Normal file
412
docs/common-workflows/multi-monitor-setup.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# Multi-Monitor Setup
|
||||
|
||||
You can set up komorebi to work with multiple monitors. To do so, first you start by setting up multiple monitor
|
||||
configurations on your `komorebi.json` config file.
|
||||
|
||||
If you've used the [`komorebic quickstart`](../cli/quickstart.md) command you'll already have a `komorebi.json` config
|
||||
file with one monitor config setup. Open this file and look for the `"monitors":` line, you should find something like
|
||||
this:
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "I",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "II",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "III",
|
||||
"layout": "HorizontalStack"
|
||||
},
|
||||
{
|
||||
"name": "IV",
|
||||
"layout": "UltrawideVerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "V",
|
||||
"layout": "Rows"
|
||||
},
|
||||
{
|
||||
"name": "VI",
|
||||
"layout": "Grid"
|
||||
},
|
||||
{
|
||||
"name": "VII",
|
||||
"layout": "RightMainVerticalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For this example we will remove some workspaces to simplify the config so it is easier to look at, but feel free to
|
||||
set up as many workspaces per monitor as you'd like. Here is the same configuration with only 3 workspaces.
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "I",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "II",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "III",
|
||||
"layout": "HorizontalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Let's add another monitor:
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
// monitor 1, index 0
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "I",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "II",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "III",
|
||||
"layout": "HorizontalStack"
|
||||
}
|
||||
]
|
||||
},
|
||||
// monitor 2, index 1
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "1",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "2",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "3",
|
||||
"layout": "HorizontalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Now have two monitor configurations. We have the first monitor configuration, which is index 0 (*usually
|
||||
on programming languages the first item of a list starts with index 0*), this configuration has 3 workspaces with names
|
||||
"I", "II" and "III". Then the 2nd monitor configuration, which is index 1, also has 3 workspaces with names "1", "2",
|
||||
and "3" (you should always give unique names to your workspaces).
|
||||
|
||||
Now if you start komorebi with two monitors connected, the main monitor will use the configuration with index 0 and the
|
||||
secondary monitor will use the configuration with index 1.
|
||||
|
||||
---
|
||||
|
||||
Let's say you have more monitors, or you want to make sure that a certain configuration is always applied to a certain
|
||||
monitor. For this you will want to use the `display_index_preferences`.
|
||||
|
||||
Open up a terminal and type the following command: [ `komorebic monitor-info`](../cli/monitor-information.md). This
|
||||
command will give you the information about your connected monitors, you want to look up the `serial_number_id`. You
|
||||
should get something like this:
|
||||
|
||||
```
|
||||
❯ komorebic monitor-info
|
||||
[
|
||||
{
|
||||
"id": 6620935,
|
||||
"name": "DISPLAY1",
|
||||
"device": "BOE0A1C",
|
||||
"device_id": "BOE0A1C-5&a2bea0b&0&UID512",
|
||||
"serial_number_id": "0",
|
||||
"size": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"right": 1920,
|
||||
"bottom": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 181932057,
|
||||
"name": "DISPLAY2",
|
||||
"device": "VSC8C31",
|
||||
"device_id": "VSC8C31-5&18560b1f&0&UID4356",
|
||||
"serial_number_id": "UEP174021562",
|
||||
"size": {
|
||||
"left": 0,
|
||||
"top": -1080,
|
||||
"right": 1920,
|
||||
"bottom": 1080
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In this case the setup is a laptop with a secondary monitor connected. You'll need to figure out which monitor is which,
|
||||
usually the display name's number should be similar to the numbers you can find on
|
||||
`Windows Settings -> System -> Display`.
|
||||
|
||||
If you have trouble with this step you can always jump on Discord and ask for help (create a `Support` thread).
|
||||
|
||||
Once you know which monitor is which, you want to look up their `serial_number_id` to use that on
|
||||
`display_index_preferences`, you can also use the `device_id`, it accepts both however there have been reported cases
|
||||
where the `device_id` changes after a restart while the `serial_number_id` doesn't.
|
||||
|
||||
So with the example above, we want the laptop to always use the configuration index 0 and the other monitor to use
|
||||
configuration index 1, so we map the configuration index number to the monitor `serial_number_id`/`device_id` like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "0",
|
||||
"1": "UEP174021562"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Again you could also have used the `device_id` like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "BOE0A1C-5&a2bea0b&0&UID512",
|
||||
"1": "VSC8C31-5&18560b1f&0&UID4356"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should add this `display_index_preferences` option to your `komorebi.json` file. If you find that something is
|
||||
not working as expected you can try to use the command `komorebic check`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> **When using multiple monitors it is recommended to always set the `display_index_preferences`. If you don't you might
|
||||
get some undefined behaviour.**
|
||||
|
||||
---
|
||||
|
||||
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
|
||||
using the `bar_configurations` array in your `komorebi.json` configuration file. You can refer to the
|
||||
[multiple-bar-instances](multiple-bar-instances.md) documentation.
|
||||
|
||||
In this case it is important to use `display_index_preferences`, because if you don't, and you have 3 or more monitors,
|
||||
disconnecting and reconnecting monitors may result in the bars for the monitors getting shifted around.
|
||||
|
||||
Consider this setup with 3 monitors (A, B and C):
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 0
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_2_BAR.json
|
||||
{
|
||||
"monitor_index": 1
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// WORK_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 2
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "MONITOR_1_ID",
|
||||
"1": "MONITOR_2_ID",
|
||||
"2": "MONITOR_3_ID"
|
||||
},
|
||||
"bar_configurations": [
|
||||
// this bar uses "monitor_index": 0,
|
||||
"path/to/bar_config_1.json",
|
||||
// this bar uses "monitor_index": 1,
|
||||
"path/to/bar_config_2.json",
|
||||
// this bar uses "monitor_index": 2,
|
||||
"path/to/bar_config_3.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Komorebi uses an internal map to keep track of monitor to config indices, this map is called `monitor_usr_idx_map` it is
|
||||
an internal variable to komorebi that you don't need to do anything with, but you can see it with the [
|
||||
`komorebic state`](../cli/state.md) command (in case you need to debug something).
|
||||
|
||||
At first, komorebi will load all monitors and set the internal index map (`monitor_usr_idx_map`) as:
|
||||
|
||||
```json
|
||||
{
|
||||
// This is monitor A
|
||||
"0": 0,
|
||||
// This is monitor B
|
||||
"1": 1,
|
||||
// This is monitor C
|
||||
"2": 2
|
||||
}
|
||||
```
|
||||
|
||||
Which kind of seems unnecessary, but imagine that then you disconnect monitor B (or it goes to sleep). Then komorebi
|
||||
will only have 2 monitors with index 0 and 1, so the above map will be updated to this:
|
||||
|
||||
```jsonc
|
||||
[
|
||||
"0": 0, // This is monitor A
|
||||
"2": 1, // This is now monitor C, because monitor B disconnected
|
||||
]
|
||||
```
|
||||
|
||||
So now the bar intended to be for monitor B, which was looking for index "1" on that map, doesn't see it and knows it
|
||||
should be disabled. And the bar for monitor C looks at that map and knows that it's index "2" now maps to index 1 so it
|
||||
uses that index internally to get all the correct values about the monitor.
|
||||
|
||||
If you didn't have the `display_index_preferences` set, then when you disconnected monitor B, komorebi wouldn't know
|
||||
how to map the indices and would use default behaviour which would result in a map like this:
|
||||
|
||||
```json
|
||||
{
|
||||
// This is monitor A
|
||||
"0": 0,
|
||||
// This is monitor C, because monitor B disconnected. However the bars will think it is monitor B because it has index "1"
|
||||
"1": 1
|
||||
}
|
||||
```
|
||||
|
||||
# Multiple Monitors on different machines
|
||||
|
||||
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
|
||||
However, if you do this it is important to be aware of a few things.
|
||||
|
||||
Firstly, using `display_index_preferences` is required in this case.
|
||||
|
||||
You will need to get the `serial_number_id` or `device_id` of all the monitors of all your setups. With that information
|
||||
you would then set your config like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "HOME_MONITOR_1_ID",
|
||||
"1": "HOME_MONITOR_2_ID",
|
||||
"2": "WORK_MONITOR_1_ID",
|
||||
"3": "WORK_MONITOR_2_ID"
|
||||
},
|
||||
"monitors": [
|
||||
// HOME_MONITOR_1
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// HOME_MONITOR_2
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// WORK_MONITOR_1
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// WORK_MONITOR_2
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> *You can't use the same config on two different monitors, you have to make a duplicated config for each monitor!*
|
||||
|
||||
Then on the bar configs you need to set the bar's monitor index like this:
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 0
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_2_BAR.json
|
||||
{
|
||||
"monitor_index": 1
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// WORK_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 2
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// WORK_MONITOR_2_BAR.json
|
||||
{
|
||||
"monitor_index": 3
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
Although you will only ever have 2 monitors connected at any one time, and they'll always have index 0 and 1, the
|
||||
above config will still work on both physical configurations.
|
||||
|
||||
This is because komorebi will apply the appropriate config to the loaded monitors and will create a map of the user
|
||||
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
|
||||
should be enabled, and where it should be drawn.
|
||||
|
||||
### Things to keep in mind
|
||||
|
||||
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
|
||||
monitor are considered different monitors by komorebi
|
||||
* When you disconnect from work, komorebi will keep the work monitor cached
|
||||
* You can still use a laptop alone without any monitor and if you need a window that was on the other monitor you can
|
||||
press the taskbar icon or use `alt + tab` to bring it to focus and that window will now be part of the laptop monitor
|
||||
* If you then reconnect the work monitor, the cached version will be applied with all its windows (except any window(s)
|
||||
you might have moved to another monitor)
|
||||
* If however, instead of reconnecting the work monitor, you connect the home monitor, then the work monitor will still
|
||||
remain cached, and komorebi will load the home monitor from the cache (if it exists)
|
||||
* Sometimes when you disconnect/reconnect a monitor the event might be missed by komorebi, meaning that Windows will
|
||||
show you both monitors but komorebi won't know about the existence of one of them
|
||||
* If you notice this type of weird behaviour, always run the [
|
||||
`komorebic monitor-info`](../cli/monitor-information.md)
|
||||
command and validate if one of the monitors is missing
|
||||
* To fix this you can try disconnecting and reconnecting the monitor again, or restarting komorebi
|
||||
@@ -8,12 +8,8 @@ configuration file.
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0,
|
||||
"border_width": 0,
|
||||
"border_offset": -1
|
||||
"default_container_padding": -1,
|
||||
}
|
||||
```
|
||||
|
||||
A restart of `komorebi` is required after changing these settings.
|
||||
|
||||
[](https://www.youtube.com/watch?v=6QYLao953XE)
|
||||
|
||||
@@ -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.
|
||||
@@ -181,10 +181,10 @@ The `grid` layout does not support resizing windows tiles.
|
||||
key bindings go to the left of the colon, and shell commands go to the right of the
|
||||
colon.
|
||||
|
||||
Please remember that `whkd` does not support overriding Microsoft's limitations
|
||||
on hotkey bindings that include the `Windows` key. If this is important to you,
|
||||
I recommend using [AutoHotKey](https://autohotkey.com) to set up your key
|
||||
bindings for `komorebic` commands instead.
|
||||
As of [`v0.2.4`](https://github.com/LGUG2Z/whkd/releases/tag/v0.2.4), `whkd` can override most of Microsoft's
|
||||
limitations on hotkey bindings that include the `win` key. However, you will still need
|
||||
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
|
||||
`win + l` from locking the operating system.
|
||||
|
||||
```
|
||||
{% include "./whkdrc.sample" %}
|
||||
@@ -203,7 +203,7 @@ It is also possible to change a hotkey behavior depending on which application h
|
||||
alt + n [
|
||||
# ProcessName as shown by `Get-Process`
|
||||
Firefox : echo "hello firefox"
|
||||
|
||||
|
||||
# Spaces are fine, no quotes required
|
||||
Google Chrome : echo "hello chrome"
|
||||
]
|
||||
@@ -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,5 +1,7 @@
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
`komorebi` is a tiling window manager that works as an extension to Microsoft's
|
||||
[Desktop Window
|
||||
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
|
||||
@@ -15,12 +17,63 @@ system and desktop environment by default. Users are free to make such
|
||||
modifications in their own configuration files for `komorebi`, but these will
|
||||
always remain opt-in and off-by-default.
|
||||
|
||||
## Community
|
||||
|
||||
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
|
||||
`komorebi`-related discussion, help, troubleshooting etc. If you have any
|
||||
specific feature requests or bugs to report, please create an issue on
|
||||
[GitHub](https://github.com/LGUG2Z/komorebi).
|
||||
`komorebi`-related discussion, help, troubleshooting etc.
|
||||
|
||||
There is a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
`komorebi` development videos, feature previews and release overviews. Subscribing
|
||||
to the channel (which is monetized as part of the YouTube Partner Program) and
|
||||
watching videos is a really simple and passive way to contribute financially to
|
||||
the development and maintenance of `komorebi`.
|
||||
|
||||
There is also a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg?sub_confirmation=1)
|
||||
where I share `komorebi` live programming videos and tutorial videos.
|
||||
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
|
||||
showcases the many awesome projects that exist in the `komorebi` ecosystem.
|
||||
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 2.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
|
||||
of the [PolyForm Strict 1.0.0 license](https://polyformproject.org/licenses/strict/1.0.0). On a high level this means
|
||||
that you are free to do whatever you want with `komorebi` for personal use other than redistribution, or distribution of
|
||||
new works (i.e. hard-forks) based on the software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either for personal use or for integration
|
||||
back upstream via pull requests.
|
||||
|
||||
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
|
||||
i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
|
||||
`komorebi` is a free and educational source project, and one that encourages you
|
||||
to make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
I encourage you to make a charitable donation to the [Palestine Children's
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
|
||||
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
|
||||
me on GitHub.
|
||||
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
|
||||
the Discord server, get shout-outs at the end of _komorebi_-related videos on
|
||||
YouTube, and gain the ability to submit feature requests on the issue tracker.
|
||||
|
||||
If you would like to tip or sponsor the project but are unable to use GitHub
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
|
||||
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
|
||||
|
||||
## Licensing for Commercial Use
|
||||
|
||||
A dedicated Individual Commercial Use License is available for those who want to
|
||||
use `komorebi` at work.
|
||||
|
||||
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
|
||||
for the licensed individual only, for the duration of a valid paid license
|
||||
subscription only. All provisions and restrictions enumerated in the [Komorebi
|
||||
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
|
||||
|
||||
More information, pricing and purchase links for Individual Commercial Use
|
||||
Licenses [can be found here](https://lgug2z.com/software/komorebi).
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
|
||||
"monitor": {
|
||||
"index": 0,
|
||||
"work_area_offset": {
|
||||
"left": 0,
|
||||
"top": 40,
|
||||
"right": 0,
|
||||
"bottom": 40
|
||||
}
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
@@ -33,6 +25,11 @@
|
||||
}
|
||||
],
|
||||
"right_widgets": [
|
||||
{
|
||||
"Update": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Media": {
|
||||
"enable": true
|
||||
@@ -73,4 +70,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
@@ -132,3 +132,19 @@ running `komorebic stop` and `komorebic start`.
|
||||
We can see the _komorebi_ state is no longer associated with the previous
|
||||
device: `null`, suggesting an issue when the display resumes from a suspended
|
||||
state.
|
||||
|
||||
## Komorebi Bar does not render transparency on Nvidia GPUs
|
||||
|
||||
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
|
||||
|
||||
To solve this the user can do the following:
|
||||
1. Open the Nvidia Control Panel
|
||||
2. On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
3. Select the "Program Settings" tab
|
||||
4. Press the "Add" button and select "komorebi-bar"
|
||||
5. Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
6. Change the setting to "Prefer compatibility"
|
||||
7. At the bottom of the window select "Apply"
|
||||
8. Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
This should resolve the issue and your Komorebi Bar should render with the proper transparency.
|
||||
|
||||
46
docs/usage/focusing-windows.md
Normal file
46
docs/usage/focusing-windows.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Focusing Windows
|
||||
|
||||
Windows can be focused in a direction (left, down, up, right) using the [`komorebic focus`](../cli/focus.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + h : komorebic focus left
|
||||
alt + j : komorebic focus down
|
||||
alt + k : komorebic focus up
|
||||
alt + l : komorebic focus right
|
||||
```
|
||||
|
||||
Windows can be focused in a cycle direction (previous, next) using the [`komorebic cycle-focus`](../cli/cycle-focus.md)
|
||||
command.
|
||||
|
||||
```
|
||||
# example showing you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-focus previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-focus next # oem_6 is ]
|
||||
```
|
||||
|
||||
It is possible to attempt to focus the first window, on any workspace, matching an exe using the [
|
||||
`komorebic eager-focus`](../cli/eager-focus.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
win + 1 : komorebic eager-focus firefox.exe
|
||||
```
|
||||
|
||||
The window at the largest tile can be focused using the [`komorebic promote-focus`](../cli/promote-focus.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + return : komorebic promote-focus
|
||||
```
|
||||
|
||||
The behaviour when attempting to call `komorebic focus` when at the left or right edge of a monitor is determined by
|
||||
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
|
||||
|
||||
When set to `Workspace`, the next workspace on the same monitor will be focused.
|
||||
|
||||
When set to `Monitor`, the focused workspace on the next monitor in the given direction will be focused.
|
||||
59
docs/usage/focusing-workspaces.md
Normal file
59
docs/usage/focusing-workspaces.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Focusing Workspaces
|
||||
|
||||
Workspaces on the focused monitor can be focused by their index using the [
|
||||
`komorebic focus-workspace`](../cli/focus-workspace.md) command.
|
||||
|
||||
If this command is called with an index for a workspace which does not exist, that workspace, and all workspace indexes
|
||||
required to get to that workspace, will be created.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + 1 : komorebic focus-workspace 0
|
||||
alt + 2 : komorebic focus-workspace 1
|
||||
alt + 3 : komorebic focus-workspace 2
|
||||
```
|
||||
|
||||
Workspaces on the focused monitor can be focused in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-workspace`](../cli/cycle-workspace.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-workspace previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-workspace next # oem_6 is ]
|
||||
```
|
||||
|
||||
Workspaces on other monitors can be focused by both the monitor index and the workspace index using the [
|
||||
`komorebic focus-monitor-workspace`](../cli/focus-monitor-workspace.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + 1 : komorebic focus-monitor-workspace 0 0
|
||||
alt + 2 : komorebic focus-monitor-workspace 0 1
|
||||
alt + 3 : komorebic focus-monitor-workspace 1 0
|
||||
```
|
||||
|
||||
Workspaces on any monitor can be focused by their name (given that all workspace names across all monitors are unique)
|
||||
using the [`komorebic focus-named-workspace`](../cli/focus-named-workspace.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + c : komorebic focus-named-workspace coding
|
||||
```
|
||||
|
||||
Workspaces on all monitors can be set to the same index (emulating single workspaces which span across all monitors)
|
||||
using the [`komorebic focus-workspaces`](../cli/focus-workspaces.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + 1 : komorebic focus-workspaces 0
|
||||
alt + 2 : komorebic focus-workspaces 1
|
||||
alt + 3 : komorebic focus-workspaces 2
|
||||
```
|
||||
|
||||
The last focused workspace on the focused monitor can be re-focused using the [
|
||||
`komorebic focus-last-workspace`](../cli/focus-last-workspace.md) command.
|
||||
59
docs/usage/moving-windows-across-workspaces.md
Normal file
59
docs/usage/moving-windows-across-workspaces.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Moving Windows Across Workspaces
|
||||
|
||||
Windows can be moved to another workspace on the focused monitor using the [
|
||||
`komorebic move-to-workspace`](../cli/move-to-workspace.md) command. This command will also move your focus to the
|
||||
target workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + 1 : komorebic move-to-workspace 0
|
||||
alt + shift + 2 : komorebic move-to-workspace 1
|
||||
alt + shift + 3 : komorebic move-to-workspace 2
|
||||
```
|
||||
|
||||
Windows can be sent to another workspace on the focused monitor using the [
|
||||
`komorebic send-to-workspace`](../cli/send-to-workspace.md) command. This command will keep your focus on the origin
|
||||
workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + 1 : komorebic send-to-workspace 0
|
||||
alt + shift + 2 : komorebic send-to-workspace 1
|
||||
alt + shift + 3 : komorebic send-to-workspace 2
|
||||
```
|
||||
|
||||
Windows can be moved to another workspace on the focused monitor in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will also move your focus
|
||||
to the target workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-move-to-workspace previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-move-to-workspace next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows can be sent to another workspace on the focused monitor in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will keep your focus on
|
||||
the origin workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-send-to-workspace previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-send-to-workspace next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows can be moved or sent to the focused workspace on a another monitor using the [
|
||||
`komorebic move-to-monitor`](../cli/move-to-monitor.md) and [`komorebic send-to-monitor`](../cli/send-to-monitor.md)
|
||||
commands.
|
||||
|
||||
Windows can be moved or sent to the focused workspace on a monitor in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-move-to-monitor`](../cli/cycle-move-to-monitor.md) and [
|
||||
`komorebic cycle-send-to-monitor`](../cli/cycle-send-to-monitor.md) commands.
|
||||
|
||||
Windows can be moved or sent to a named workspace on any monitor (given that all workspace names across all monitors are
|
||||
unique) using the [`komorebic move-to-named-workspace`](../cli/move-to-named-workspace.md) and [
|
||||
`komorebic send-to-named-workspace`](../cli/send-to-named-workspace.md) commands
|
||||
50
docs/usage/moving-windows.md
Normal file
50
docs/usage/moving-windows.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Moving Windows
|
||||
|
||||
Windows can be moved in a direction (left, down, up, right) using the [`komorebic move`](../cli/move.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + h : komorebic move left
|
||||
alt + shift + j : komorebic move down
|
||||
alt + shift + k : komorebic move up
|
||||
alt + shift + l : komorebic move right
|
||||
```
|
||||
|
||||
Windows can be moved in a cycle direction (previous, next) using the [`komorebic cycle-move`](../cli/cycle-move.md)
|
||||
command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-move previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-move next # oem_6 is ]
|
||||
```
|
||||
|
||||
The focused window can be moved to the largest tile using the [`komorebic promote`](../cli/promote.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + return : komorebic promote
|
||||
```
|
||||
|
||||
The behaviour when attempting to call `komorebic move` when at the left or right edge of a monitor is determined by
|
||||
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
|
||||
|
||||
When set to `Workspace`, the focused window will be moved to the next workspace on the focused monitor in the given
|
||||
direction
|
||||
|
||||
When set to `Monitor`, the focused window will be moved to the focused workspace on the next monitor in the given
|
||||
direction.
|
||||
|
||||
The behaviour when calling `komorebic move` with `cross_boundary_behaviour` set to `Monitor` can be further refined with
|
||||
the [`cross_monitor_move_behaviour`](https://komorebi.lgug2z.com/schema#cross_monitor_move_behaviour) configuration
|
||||
option.
|
||||
|
||||
When set to `Swap`, the focused window will be swapped with the window at the corresponding edge of the adjacent monitor
|
||||
|
||||
When set to `Insert`, the focused window will be inserted into the focused workspace on the adjacent monitor.
|
||||
|
||||
When set to `NoOp`, the focused window will not be moved across a monitor boundary, though focusing across monitor
|
||||
boundaries will continue to function.
|
||||
52
docs/usage/stacking-windows.md
Normal file
52
docs/usage/stacking-windows.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Stacking Windows
|
||||
|
||||
Windows can be stacked in a direction (left, down, up, right) using the [`komorebic stack`](../cli/stack.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + left : komorebic stack left
|
||||
alt + down : komorebic stack down
|
||||
alt + up : komorebic stack up
|
||||
alt + right : komorebic stack right
|
||||
```
|
||||
|
||||
Windows can be popped from a stack using the [`komorebic unstack`](../cli/unstack.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + oem_1 : komorebic unstack # oem_1 is ;
|
||||
```
|
||||
|
||||
Windows in a stack can be focused in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-stack`](../cli/cycle-stack.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + oem_4 : komorebic cycle-stack previous # oem_4 is [
|
||||
alt + oem_6 : komorebic cycle-stack next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows in a stack can have their positions in the stack moved in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-stack-index`](../cli/cycle-stack-index.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-stack-index previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-stack-index next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows in a stack can be focused by their index in the stack using the [
|
||||
`komorebic focus-stack-window`](../cli/focus-stack-window.md) command.
|
||||
|
||||
All windows on the focused workspace can be combined into a single stack using the [
|
||||
`komorebic stack-all`](../cli/stack-all.md) command.
|
||||
|
||||
All windows in a focused stack can be popped using the [`komorebic unstack-all`](../cli/unstack-all.md) command.
|
||||
|
||||
It is possible to tell the window manager to stack the next opened window on top of the currently focused window by
|
||||
using the [
|
||||
`komorebic toggle-workspace-window-container-behaviour`](../cli/toggle-workspace-window-container-behaviour.md) command.
|
||||
49
justfile
49
justfile
@@ -1,4 +1,5 @@
|
||||
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
|
||||
|
||||
export RUST_BACKTRACE := "full"
|
||||
|
||||
clean:
|
||||
@@ -18,13 +19,43 @@ install-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
||||
|
||||
install-target target:
|
||||
cargo +stable install --path {{ target }} --locked --no-default-features
|
||||
|
||||
install-targets-with-jsonschema *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target-with-jsonschema $_ }
|
||||
|
||||
install-target-with-jsonschema target:
|
||||
cargo +stable install --path {{ target }} --locked
|
||||
|
||||
install:
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
install-with-jsonschema:
|
||||
just install-targets-with-jsonschema 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 --package {{ target }} --locked --release --no-default-features
|
||||
|
||||
build:
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
copy-target target:
|
||||
cp .\target\release\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
|
||||
|
||||
copy-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just copy-target $_ }
|
||||
|
||||
wpm target:
|
||||
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
|
||||
|
||||
copy:
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked
|
||||
cargo +stable run --bin {{ target }} --locked --no-default-features
|
||||
|
||||
warn target $RUST_LOG="warn":
|
||||
just run {{ target }}
|
||||
@@ -39,19 +70,21 @@ trace target $RUST_LOG="trace":
|
||||
just run {{ target }}
|
||||
|
||||
deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
|
||||
|
||||
docgen:
|
||||
cargo run --package komorebic -- docgen
|
||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
||||
|
||||
schemagen:
|
||||
jsonschema:
|
||||
cargo run --package komorebic -- static-config-schema > schema.json
|
||||
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
|
||||
cargo run --package komorebi-bar -- --schema > schema.bar.json
|
||||
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
|
||||
|
||||
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
|
||||
|
||||
rm -Force .\bar-config-docs\schema.html
|
||||
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html
|
||||
# this part is run in a nix shell because python is a nightmare
|
||||
schemagen:
|
||||
rm -rf static-config-docs bar-config-docs
|
||||
mkdir -p static-config-docs bar-config-docs
|
||||
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
|
||||
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
|
||||
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.31"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -9,6 +9,7 @@ edition = "2021"
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
chrono-tz = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
@@ -16,16 +17,18 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.7"
|
||||
egui-phosphor = "0.9"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
netdev = "0.31"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.33"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
schemars = { workspace = true }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
starship-battery = "0.10"
|
||||
@@ -33,4 +36,10 @@ sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
windows-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
|
||||
windows-icons-fallback = { package = "windows-icons", git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,160 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use starship_battery::Manager;
|
||||
use starship_battery::State;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BatteryConfig {
|
||||
/// Enable the Battery widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
fn from(value: BatteryConfig) -> Self {
|
||||
let manager = Manager::new().unwrap();
|
||||
let mut last_state = String::new();
|
||||
let mut state = None;
|
||||
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
|
||||
|
||||
if let Ok(mut batteries) = manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
match first.state() {
|
||||
State::Charging => state = Some(BatteryState::Charging),
|
||||
State::Discharging => state = Some(BatteryState::Discharging),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
last_state = match prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
manager,
|
||||
last_state,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: prefix,
|
||||
state: state.unwrap_or(BatteryState::Discharging),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BatteryState {
|
||||
Charging,
|
||||
Discharging,
|
||||
}
|
||||
|
||||
pub struct Battery {
|
||||
pub enable: bool,
|
||||
manager: Manager,
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_state: String,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Battery {
|
||||
fn output(&mut self) -> String {
|
||||
let mut output = self.last_state.clone();
|
||||
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
output.clear();
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => self.state = BatteryState::Discharging,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
output = match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let emoji = match self.state {
|
||||
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
|
||||
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
|
||||
};
|
||||
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,85 @@
|
||||
use crate::render::Grouping;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::Rect;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.31`
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.36`
|
||||
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)
|
||||
pub font_size: Option<f32>,
|
||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
|
||||
pub icon_scale: Option<f32>,
|
||||
/// Max label width before text truncation (default: 400.0)
|
||||
pub max_label_width: Option<f32>,
|
||||
/// Theme
|
||||
@@ -70,9 +125,19 @@ impl KomobarConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_all_icons_on_komorebi_workspace(widgets: &[WidgetConfig]) -> bool {
|
||||
widgets
|
||||
.iter()
|
||||
.any(|w| matches!(w, WidgetConfig::Komorebi(config) if config.workspaces.is_some_and(|w| w.enable && w.display.is_some_and(|s| matches!(s,
|
||||
WorkspacesDisplayFormat::AllIcons
|
||||
| WorkspacesDisplayFormat::AllIconsAndText
|
||||
| WorkspacesDisplayFormat::AllIconsAndTextOnSelected)))))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct PositionConfig {
|
||||
/// The desired starting position of the bar (0,0 = top left of the screen)
|
||||
#[serde(alias = "position")]
|
||||
@@ -82,13 +147,25 @@ pub struct PositionConfig {
|
||||
pub end: Option<Position>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct FrameConfig {
|
||||
/// Margin inside the painted frame
|
||||
pub inner_margin: Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::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)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct MonitorConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub index: usize,
|
||||
@@ -96,6 +173,158 @@ pub struct MonitorConfig {
|
||||
pub work_area_offset: Option<Rect>,
|
||||
}
|
||||
|
||||
pub type Padding = SpacingKind;
|
||||
pub type Margin = SpacingKind;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::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)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct GroupedSpacingConfig {
|
||||
pub vertical: Option<GroupedSpacingOptions>,
|
||||
pub horizontal: Option<GroupedSpacingOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum GroupedSpacingOptions {
|
||||
Symmetrical(f32),
|
||||
Split(f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::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)?;
|
||||
@@ -106,7 +335,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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,7 +346,8 @@ impl KomobarConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Position {
|
||||
/// X coordinate
|
||||
pub x: f32,
|
||||
@@ -140,7 +373,8 @@ impl From<Position> for Pos2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
@@ -148,12 +382,24 @@ pub enum KomobarTheme {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_text: 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,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
/// A custom Base16 theme
|
||||
Custom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
colours: Box<komorebi_themes::Base16ColourPalette>,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -165,18 +411,33 @@ impl From<KomorebiTheme> for KomobarTheme {
|
||||
} => Self::Catppuccin {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Base16 {
|
||||
name, bar_accent, ..
|
||||
} => Self::Base16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Custom {
|
||||
colours,
|
||||
bar_accent,
|
||||
..
|
||||
} => Self::Custom {
|
||||
colours,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum LabelPrefix {
|
||||
/// Show no prefix
|
||||
None,
|
||||
@@ -188,12 +449,105 @@ pub enum LabelPrefix {
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DisplayFormat {
|
||||
/// Show only icon
|
||||
Icon,
|
||||
/// Show only text
|
||||
Text,
|
||||
/// Show an icon and text for the selected element, and text on the rest
|
||||
TextAndIconOnSelected,
|
||||
/// Show both icon and text
|
||||
IconAndText,
|
||||
/// Show an icon and text for the selected element, and icons on the rest
|
||||
IconAndTextOnSelected,
|
||||
}
|
||||
|
||||
macro_rules! extend_enum {
|
||||
($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum $new_enum {
|
||||
// Add new variants
|
||||
$(
|
||||
$(#[$meta])*
|
||||
$variant,
|
||||
)*
|
||||
// Include a variant that wraps the existing enum and flatten it when deserializing
|
||||
#[serde(untagged)]
|
||||
Existing($existing_enum),
|
||||
}
|
||||
|
||||
// Implement From for the existing enum
|
||||
impl From<$existing_enum> for $new_enum {
|
||||
fn from(value: $existing_enum) -> Self {
|
||||
$new_enum::Existing(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
|
||||
/// Show all icons only
|
||||
AllIcons,
|
||||
/// Show both all icons and text
|
||||
AllIconsAndText,
|
||||
/// Show all icons and text for the selected element, and all icons on the rest
|
||||
AllIconsAndTextOnSelected,
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum OriginalDisplayFormat {
|
||||
/// Show None Of The Things
|
||||
NoneOfTheThings,
|
||||
}
|
||||
|
||||
extend_enum!(OriginalDisplayFormat, ExtendedDisplayFormat, {
|
||||
/// Show Some Of The Things
|
||||
SomeOfTheThings,
|
||||
});
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ExampleConfig {
|
||||
#[allow(unused)]
|
||||
format: ExtendedDisplayFormat,
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn extend_new_variant() {
|
||||
let raw = json!({
|
||||
"format": "SomeOfTheThings",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
assert!(serde_json::from_str::<ExampleConfig>(&raw).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn extend_existing_variant() {
|
||||
let raw = json!({
|
||||
"format": "NoneOfTheThings",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
assert!(serde_json::from_str::<ExampleConfig>(&raw).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn extend_invalid_variant() {
|
||||
let raw = json!({
|
||||
"format": "ALLOFTHETHINGS",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
assert!(serde_json::from_str::<ExampleConfig>(&raw).is_err())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
pub enable: bool,
|
||||
/// Set the Date format
|
||||
pub format: DateFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<DateConfig> for Date {
|
||||
fn from(value: DateConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum DateFormat {
|
||||
/// Month/Date/Year format (09/08/24)
|
||||
MonthDateYear,
|
||||
/// Year-Month-Date format (2024-09-08)
|
||||
YearMonthDate,
|
||||
/// Date-Month-Year format (8-Sep-2024)
|
||||
DateMonthYear,
|
||||
/// Day Date Month Year format (8 September 2024)
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl DateFormat {
|
||||
pub fn next(&mut self) {
|
||||
match self {
|
||||
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
|
||||
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
|
||||
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
|
||||
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Date {
|
||||
pub enable: bool,
|
||||
pub format: DateFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Date {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CALENDAR_DOTS.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "DATE: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,645 +0,0 @@
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::komorebi_layout::KomorebiLayout;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
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;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::Container;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::Window;
|
||||
use komorebi_client::Workspace;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiConfig {
|
||||
/// Configure the Workspaces widget
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
/// Configure the Layout widget
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
/// Configure the Focused Window widget
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
/// Configure the Configuration Switcher widget
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiWorkspacesConfig {
|
||||
/// Enable the Komorebi Workspaces widget
|
||||
pub enable: bool,
|
||||
/// Hide workspaces without any windows
|
||||
pub hide_empty_workspaces: bool,
|
||||
/// Display format of the workspace
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiLayoutConfig {
|
||||
/// Enable the Komorebi Layout widget
|
||||
pub enable: bool,
|
||||
/// List of layout options
|
||||
pub options: Option<Vec<KomorebiLayout>>,
|
||||
/// Display format of the current layout
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiFocusedWindowConfig {
|
||||
/// Enable the Komorebi Focused Window widget
|
||||
pub enable: bool,
|
||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
|
||||
pub show_icon: Option<bool>,
|
||||
/// Display format of the currently focused window
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiConfigurationSwitcherConfig {
|
||||
/// Enable the Komorebi Configurations widget
|
||||
pub enable: bool,
|
||||
/// A map of display friendly name => path to configuration.json
|
||||
pub configurations: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl From<&KomorebiConfig> for Komorebi {
|
||||
fn from(value: &KomorebiConfig) -> Self {
|
||||
let configuration_switcher =
|
||||
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()))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
Some(configuration_switcher)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
|
||||
selected_workspace: String::new(),
|
||||
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
workspaces: vec![],
|
||||
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
|
||||
stack_accent: None,
|
||||
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout.clone(),
|
||||
focused_window: value.focused_window,
|
||||
configuration_switcher,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Komorebi {
|
||||
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
impl BarWidget for Komorebi {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
|
||||
|
||||
if self.workspaces.enable {
|
||||
let mut update = None;
|
||||
|
||||
if !komorebi_notification_state.workspaces.is_empty() {
|
||||
let format = self.workspaces.display.unwrap_or(DisplayFormat::Text);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, container_information)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
if SelectableFrame::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
)
|
||||
.show(ui, |ui| {
|
||||
let mut has_icon = false;
|
||||
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
let icons: Vec<_> =
|
||||
container_information.icons.iter().flatten().collect();
|
||||
|
||||
if !icons.is_empty() {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
for icon in icons {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, icon))
|
||||
.maintain_aspect_ratio(true)
|
||||
.shrink_to_fit(),
|
||||
);
|
||||
|
||||
if !has_icon {
|
||||
has_icon = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// draw a custom icon when there is no app icon
|
||||
if match format {
|
||||
DisplayFormat::Icon => !has_icon,
|
||||
_ => false,
|
||||
} {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::splat(font_id.size), Sense::hover());
|
||||
let stroke =
|
||||
Stroke::new(1.0, ctx.style().visuals.selection.stroke.color);
|
||||
let mut rect = response.rect;
|
||||
let rounding = Rounding::same(rect.width() * 0.1);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke);
|
||||
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
|
||||
|
||||
response.on_hover_text(ws.to_string())
|
||||
} else if match format {
|
||||
DisplayFormat::Icon => has_icon,
|
||||
_ => false,
|
||||
} {
|
||||
ui.response().on_hover_text(ws.to_string())
|
||||
} else {
|
||||
ui.add(Label::new(ws.to_string()).selectable(false))
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(
|
||||
&SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusWorkspaceNumber"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(
|
||||
&SocketMessage::RetileWithResizeDimensions,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(layout_config) = &self.layout {
|
||||
if layout_config.enable {
|
||||
let workspace_idx: Option<usize> = komorebi_notification_state
|
||||
.workspaces
|
||||
.iter()
|
||||
.position(|o| komorebi_notification_state.selected_workspace.eq(&o.0));
|
||||
|
||||
komorebi_notification_state.layout.show(
|
||||
ctx,
|
||||
ui,
|
||||
config,
|
||||
layout_config,
|
||||
workspace_idx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(configuration_switcher) = &self.configuration_switcher {
|
||||
if configuration_switcher.enable {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file() {
|
||||
config.apply_on_widget(false, ui,|ui|{
|
||||
if SelectableFrame::new(false).show(ui, |ui|{
|
||||
ui.add(Label::new(name).selectable(false))
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
let mut proceed = true;
|
||||
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
|
||||
canonicalized,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MonitorWorkAreaOffset"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_window) = self.focused_window {
|
||||
if focused_window.enable {
|
||||
let titles = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.titles;
|
||||
if !titles.is_empty() {
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
let icons = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.icons;
|
||||
let focused_window_idx = komorebi_notification_state
|
||||
.focused_container_information
|
||||
.focused_window_idx;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
let selected = i == focused_window_idx;
|
||||
|
||||
if SelectableFrame::new(selected)
|
||||
.show(ui, |ui| {
|
||||
// handle legacy setting
|
||||
let format = focused_window.display.unwrap_or(
|
||||
if focused_window.show_icon.unwrap_or(false) {
|
||||
DisplayFormat::IconAndText
|
||||
} else {
|
||||
DisplayFormat::Text
|
||||
},
|
||||
);
|
||||
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format
|
||||
{
|
||||
if let Some(img) = icon {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
let response = ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.shrink_to_fit(),
|
||||
);
|
||||
|
||||
if let DisplayFormat::Icon = format {
|
||||
response.on_hover_text(title);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format
|
||||
{
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
if selected {
|
||||
return;
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(
|
||||
i,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
|
||||
ctx.load_texture("icon", color_image, TextureOptions::default())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: KomorebiNotificationStateContainerInformation,
|
||||
pub layout: KomorebiLayout,
|
||||
pub hide_empty_workspaces: bool,
|
||||
pub mouse_follows_focus: bool,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub stack_accent: Option<Color32>,
|
||||
pub monitor_index: usize,
|
||||
}
|
||||
|
||||
impl KomorebiNotificationState {
|
||||
pub fn update_from_config(&mut self, config: &Self) {
|
||||
self.hide_empty_workspaces = config.hide_empty_workspaces;
|
||||
}
|
||||
|
||||
pub fn handle_notification(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
monitor_index: usize,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
) {
|
||||
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());
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
self.focused_container_information =
|
||||
(&monitor.workspaces()[focused_workspace_idx]).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationStateContainerInformation {
|
||||
pub titles: Vec<String>,
|
||||
pub icons: Vec<Option<RgbaImage>>,
|
||||
pub focused_window_idx: usize,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Workspace) -> Self {
|
||||
let mut container_info = Self::EMPTY;
|
||||
|
||||
if let Some(container) = value.monocle_container() {
|
||||
container_info = container.into();
|
||||
} else if let Some(container) = value.focused_container() {
|
||||
container_info = container.into();
|
||||
}
|
||||
|
||||
for floating_window in value.floating_windows() {
|
||||
if floating_window.is_focused() {
|
||||
container_info = floating_window.into();
|
||||
}
|
||||
}
|
||||
|
||||
container_info
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Container) -> Self {
|
||||
Self {
|
||||
titles: value
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
icons: value
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
focused_window_idx: value.focused_window_idx(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Window) -> Self {
|
||||
Self {
|
||||
titles: vec![value.title().unwrap_or_default()],
|
||||
icons: vec![windows_icons::get_icon_by_process_id(value.process_id())],
|
||||
focused_window_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiNotificationStateContainerInformation {
|
||||
pub const EMPTY: Self = Self {
|
||||
titles: vec![],
|
||||
icons: vec![],
|
||||
focused_window_idx: 0,
|
||||
};
|
||||
}
|
||||
@@ -1,42 +1,37 @@
|
||||
mod bar;
|
||||
mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod komorebi;
|
||||
mod komorebi_layout;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod render;
|
||||
mod selected_frame;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
mod widget;
|
||||
mod widgets;
|
||||
|
||||
use crate::bar::Komobar;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use clap::Parser;
|
||||
use config::MonitorConfigOrIndex;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::replace_env_in_path;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use schemars::gen::SchemaSettings;
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
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;
|
||||
@@ -45,6 +40,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);
|
||||
@@ -52,6 +48,13 @@ 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 AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
@@ -64,6 +67,7 @@ struct Opts {
|
||||
fonts: bool,
|
||||
/// Path to a JSON or YAML configuration file
|
||||
#[clap(short, long)]
|
||||
#[clap(value_parser = replace_env_in_path)]
|
||||
config: Option<PathBuf>,
|
||||
/// Write an example komorebi.bar.json to disk
|
||||
#[clap(long)]
|
||||
@@ -105,13 +109,19 @@ 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) }?;
|
||||
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
if opts.schema {
|
||||
let settings = SchemaSettings::default().with(|s| {
|
||||
let settings = schemars::gen::SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = false;
|
||||
s.option_add_null_type = false;
|
||||
s.inline_subschemas = true;
|
||||
@@ -152,13 +162,15 @@ fn main() -> color_eyre::Result<()> {
|
||||
let 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);
|
||||
let home = home_path.replace_env();
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
|
||||
}
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
);
|
||||
|
||||
home
|
||||
},
|
||||
);
|
||||
|
||||
@@ -167,7 +179,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
println!(
|
||||
"Example komorebi.bar.json file written to {}",
|
||||
home_dir.as_path().display()
|
||||
home_dir.display()
|
||||
);
|
||||
|
||||
std::process::exit(0);
|
||||
@@ -175,16 +187,11 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let default_config_path = home_dir.join("komorebi.bar.json");
|
||||
|
||||
let config_path = opts.config.map_or_else(
|
||||
|| {
|
||||
if !default_config_path.is_file() {
|
||||
None
|
||||
} else {
|
||||
Some(default_config_path.clone())
|
||||
}
|
||||
},
|
||||
Option::from,
|
||||
);
|
||||
let config_path = opts.config.or_else(|| {
|
||||
default_config_path
|
||||
.is_file()
|
||||
.then_some(default_config_path.clone())
|
||||
});
|
||||
|
||||
let mut config = match config_path {
|
||||
None => {
|
||||
@@ -194,17 +201,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
std::fs::write(&default_config_path, komorebi_bar_json)?;
|
||||
tracing::info!(
|
||||
"created example configuration file: {}",
|
||||
default_config_path.as_path().display()
|
||||
default_config_path.display()
|
||||
);
|
||||
|
||||
KomobarConfig::read(&default_config_path)?
|
||||
}
|
||||
Some(ref config) => {
|
||||
if !opts.aliases {
|
||||
tracing::info!(
|
||||
"found configuration file: {}",
|
||||
config.as_path().to_string_lossy()
|
||||
);
|
||||
tracing::info!("found configuration file: {}", config.display());
|
||||
}
|
||||
|
||||
KomobarConfig::read(config)?
|
||||
@@ -222,32 +226,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,
|
||||
}),
|
||||
})
|
||||
@@ -255,14 +270,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,
|
||||
})
|
||||
}
|
||||
@@ -279,15 +294,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();
|
||||
@@ -299,10 +308,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
hotwatch.watch(config_path, move |event| match event.kind {
|
||||
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
|
||||
Ok(updated) => {
|
||||
tracing::info!(
|
||||
"configuration file updated: {}",
|
||||
config_path_cl.as_path().to_string_lossy()
|
||||
);
|
||||
tracing::info!("configuration file updated: {}", config_path_cl.display());
|
||||
|
||||
if let Err(error) = tx_config.send(updated) {
|
||||
tracing::error!("could not send configuration update to gui: {error}")
|
||||
@@ -317,13 +323,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));
|
||||
@@ -332,7 +335,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let ctx_komorebi = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::get(random_word::Lang::En));
|
||||
|
||||
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
|
||||
filter_state_changes: true,
|
||||
@@ -344,6 +347,10 @@ fn main() -> color_eyre::Result<()> {
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(subscription) => {
|
||||
match subscription.set_read_timeout(Some(Duration::from_secs(1))) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(subscription);
|
||||
|
||||
@@ -362,18 +369,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) {
|
||||
@@ -384,7 +385,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}")
|
||||
}
|
||||
|
||||
@@ -409,7 +410,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()))
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use num_derive::FromPrimitive;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
/// Show total data transmitted
|
||||
pub show_total_data_transmitted: bool,
|
||||
/// Show network activity
|
||||
pub show_network_activity: bool,
|
||||
/// Characters to reserve for network activity data
|
||||
pub network_activity_fill_characters: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
show_total_activity: value.show_total_data_transmitted,
|
||||
show_activity: value.show_network_activity,
|
||||
network_activity_fill_characters: value
|
||||
.network_activity_fill_characters
|
||||
.unwrap_or_default(),
|
||||
last_state_total_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
default_interface: String,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
network_activity_fill_characters: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh();
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if self.show_total_activity {
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
Self::to_pretty_bytes(data.total_received(), 1),
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn reading_to_label(&self, ctx: &Context, reading: NetworkReading) -> Label {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s | ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} | ", reading.received_text),
|
||||
reading.transmitted_text,
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s | ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s | ", reading.received_text),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted_text),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let icon_format =
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.selection.stroke.color);
|
||||
let text_format = TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color());
|
||||
|
||||
// icon
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
icon_format.font_id.clone(),
|
||||
icon_format.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_down,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
// icon
|
||||
layout_job.append(
|
||||
&match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_UP.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
0.0,
|
||||
icon_format.clone(),
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_up,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
Label::new(layout_job).selectable(false)
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.show_total_activity || self.show_activity {
|
||||
let (activity, total_activity) = self.network_activity();
|
||||
|
||||
if self.show_total_activity {
|
||||
for reading in total_activity {
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_activity {
|
||||
for reading in activity {
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.enable {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum NetworkReadingFormat {
|
||||
Speed = 0,
|
||||
Total = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NetworkReading {
|
||||
pub format: NetworkReadingFormat,
|
||||
pub received_text: String,
|
||||
pub transmitted_text: String,
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
|
||||
NetworkReading {
|
||||
format,
|
||||
received_text: received,
|
||||
transmitted_text: transmitted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPrimitive)]
|
||||
enum DataUnit {
|
||||
B = 0,
|
||||
K = 1,
|
||||
M = 2,
|
||||
G = 3,
|
||||
T = 4,
|
||||
P = 5,
|
||||
E = 6,
|
||||
Z = 7,
|
||||
Y = 8,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,31 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::InnerResponse;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use schemars::JsonSchema;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::Rgb;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum Grouping {
|
||||
/// No grouping is applied
|
||||
@@ -29,7 +38,7 @@ pub enum Grouping {
|
||||
Widget(GroupingConfig),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct RenderConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub monitor_idx: usize,
|
||||
@@ -45,22 +54,73 @@ pub struct RenderConfig {
|
||||
pub more_inner_margin: bool,
|
||||
/// Set to true after the first time the apply_on_widget was called on an alignment
|
||||
pub applied_on_widget: bool,
|
||||
/// FontId for text
|
||||
pub text_font_id: FontId,
|
||||
/// FontId for icon (based on scaling the text font id)
|
||||
pub icon_font_id: FontId,
|
||||
/// Show all icons on the workspace section of the Komorebi widget
|
||||
pub show_all_icons: bool,
|
||||
/// Background color of the selected frame
|
||||
pub auto_select_fill: Option<Color32>,
|
||||
/// Text color of the selected frame
|
||||
pub auto_select_text: Option<Color32>,
|
||||
}
|
||||
|
||||
pub trait RenderExt {
|
||||
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig;
|
||||
fn new_renderconfig(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
background_color: Color32,
|
||||
icon_scale: Option<f32>,
|
||||
) -> RenderConfig;
|
||||
}
|
||||
|
||||
impl RenderExt for &KomobarConfig {
|
||||
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
|
||||
fn new_renderconfig(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
background_color: Color32,
|
||||
icon_scale: Option<f32>,
|
||||
) -> RenderConfig {
|
||||
let text_font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut icon_font_id = text_font_id.clone();
|
||||
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
||||
|
||||
let monitor_idx = match &self.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
};
|
||||
|
||||
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
|
||||
let show_all_icons =
|
||||
KomobarConfig::show_all_icons_on_komorebi_workspace(&self.left_widgets)
|
||||
|| self
|
||||
.center_widgets
|
||||
.as_ref()
|
||||
.is_some_and(|list| KomobarConfig::show_all_icons_on_komorebi_workspace(list))
|
||||
|| KomobarConfig::show_all_icons_on_komorebi_workspace(&self.right_widgets);
|
||||
|
||||
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,
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id,
|
||||
icon_font_id,
|
||||
show_all_icons,
|
||||
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
|
||||
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
|
||||
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
|
||||
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,21 +143,36 @@ impl RenderConfig {
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id: FontId::default(),
|
||||
icon_font_id: FontId::default(),
|
||||
show_all_icons: false,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_on_bar<R>(
|
||||
pub fn change_frame_on_bar(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
frame: Frame,
|
||||
ui_style: &Arc<eframe::egui::Style>,
|
||||
) -> Frame {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Bar(config) = self.grouping {
|
||||
return self.define_group(None, config, ui, add_contents);
|
||||
return self.define_group_frame(
|
||||
//TODO: this outer margin can be a config
|
||||
Some(Margin {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
}),
|
||||
config,
|
||||
ui_style,
|
||||
);
|
||||
}
|
||||
|
||||
Self::fallback_group(ui, add_contents)
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn apply_on_alignment<R>(
|
||||
@@ -143,11 +218,11 @@ impl RenderConfig {
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
Frame::none()
|
||||
Frame::NONE
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(5.0, 0.0),
|
||||
false => Margin::same(0.0),
|
||||
true => Margin::symmetric(5, 0),
|
||||
false => Margin::same(0),
|
||||
})
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
@@ -159,16 +234,26 @@ impl RenderConfig {
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
Frame::group(ui.style_mut())
|
||||
self.define_group_frame(outer_margin, config, ui.style())
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn define_group_frame(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
config: GroupingConfig,
|
||||
ui_style: &Arc<eframe::egui::Style>,
|
||||
) -> Frame {
|
||||
Frame::group(ui_style)
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(8.0, 3.0),
|
||||
false => Margin::symmetric(3.0, 3.0),
|
||||
true => Margin::symmetric(6, 1),
|
||||
false => Margin::symmetric(1, 1),
|
||||
})
|
||||
.stroke(ui.style().visuals.widgets.noninteractive.bg_stroke)
|
||||
.rounding(match config.rounding {
|
||||
.stroke(ui_style.visuals.widgets.noninteractive.bg_stroke)
|
||||
.corner_radius(match config.rounding {
|
||||
Some(rounding) => rounding.into(),
|
||||
None => ui.style().visuals.widgets.noninteractive.rounding,
|
||||
None => ui_style.visuals.widgets.noninteractive.corner_radius,
|
||||
})
|
||||
.fill(
|
||||
self.background_color
|
||||
@@ -178,24 +263,68 @@ impl RenderConfig {
|
||||
Some(style) => match style {
|
||||
// new styles can be added if needed here
|
||||
GroupingStyle::Default => Shadow::NONE,
|
||||
GroupingStyle::DefaultWithShadow => Shadow {
|
||||
blur: 4.0,
|
||||
offset: Vec2::new(1.0, 1.0),
|
||||
spread: 3.0,
|
||||
GroupingStyle::DefaultWithShadowB4O1S3 => Shadow {
|
||||
blur: 4,
|
||||
offset: [1, 1],
|
||||
spread: 3,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithShadowB4O0S3 => Shadow {
|
||||
blur: 4,
|
||||
offset: [0, 0],
|
||||
spread: 3,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithShadowB0O1S3 => Shadow {
|
||||
blur: 0,
|
||||
offset: [1, 1],
|
||||
spread: 3,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB3O1S2 => Shadow {
|
||||
blur: 3,
|
||||
offset: [1, 1],
|
||||
spread: 2,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB3O0S2 => Shadow {
|
||||
blur: 3,
|
||||
offset: [0, 0],
|
||||
spread: 2,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB0O1S2 => Shadow {
|
||||
blur: 0,
|
||||
offset: [1, 1],
|
||||
spread: 2,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
},
|
||||
None => Shadow::NONE,
|
||||
})
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin {
|
||||
let spacing = if self.applied_on_widget {
|
||||
// Remove the default item spacing from the margin
|
||||
self.spacing - ui.spacing().item_spacing.x
|
||||
(self.spacing - ui.spacing().item_spacing.x) as i8
|
||||
} else {
|
||||
0.0
|
||||
0
|
||||
};
|
||||
|
||||
if !self.applied_on_widget {
|
||||
@@ -207,25 +336,26 @@ impl RenderConfig {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => spacing,
|
||||
Alignment::Center => spacing,
|
||||
Alignment::Right => 0.0,
|
||||
Alignment::Right => 0,
|
||||
},
|
||||
None => 0.0,
|
||||
None => 0,
|
||||
},
|
||||
right: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => 0.0,
|
||||
Alignment::Center => 0.0,
|
||||
Alignment::Left => 0,
|
||||
Alignment::Center => 0,
|
||||
Alignment::Right => spacing,
|
||||
},
|
||||
None => 0.0,
|
||||
None => 0,
|
||||
},
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct GroupingConfig {
|
||||
/// Styles for the grouping
|
||||
pub style: Option<GroupingStyle>,
|
||||
@@ -235,16 +365,29 @@ pub struct GroupingConfig {
|
||||
pub rounding: Option<RoundingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum GroupingStyle {
|
||||
#[serde(alias = "CtByte")]
|
||||
Default,
|
||||
/// A black shadow is added under the default group
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
||||
#[serde(alias = "CtByteWithShadow")]
|
||||
DefaultWithShadow,
|
||||
#[serde(alias = "DefaultWithShadow")]
|
||||
DefaultWithShadowB4O1S3,
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-0 y-0, spread: 3)
|
||||
DefaultWithShadowB4O0S3,
|
||||
/// A shadow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 3)
|
||||
DefaultWithShadowB0O1S3,
|
||||
/// A glow is added under the default group. (blur: 3, offset: x-1 y-1, spread: 2)
|
||||
DefaultWithGlowB3O1S2,
|
||||
/// A glow is added under the default group. (blur: 3, offset: x-0 y-0, spread: 2)
|
||||
DefaultWithGlowB3O0S2,
|
||||
/// A glow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 2)
|
||||
DefaultWithGlowB0O1S2,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum RoundingConfig {
|
||||
/// All 4 corners are the same
|
||||
@@ -253,16 +396,19 @@ pub enum RoundingConfig {
|
||||
Individual([f32; 4]),
|
||||
}
|
||||
|
||||
impl From<RoundingConfig> for Rounding {
|
||||
impl From<RoundingConfig> for CornerRadius {
|
||||
fn from(value: RoundingConfig) -> Self {
|
||||
match value {
|
||||
RoundingConfig::Same(value) => Rounding::same(value),
|
||||
RoundingConfig::Individual(values) => Self {
|
||||
nw: values[0],
|
||||
ne: values[1],
|
||||
sw: values[2],
|
||||
se: values[3],
|
||||
},
|
||||
RoundingConfig::Same(value) => Self::same(value as u8),
|
||||
RoundingConfig::Individual(values) => {
|
||||
let values = values.map(|f| f as u8);
|
||||
Self {
|
||||
nw: values[0],
|
||||
ne: values[1],
|
||||
sw: values[2],
|
||||
se: values[3],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,81 @@
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::CursorIcon;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Response;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::Ui;
|
||||
|
||||
/// Same as SelectableLabel, but supports all content
|
||||
pub struct SelectableFrame {
|
||||
selected: bool,
|
||||
selected_fill: Option<Color32>,
|
||||
}
|
||||
|
||||
impl SelectableFrame {
|
||||
pub fn new(selected: bool) -> Self {
|
||||
Self { selected }
|
||||
Self {
|
||||
selected,
|
||||
selected_fill: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_auto(selected: bool, selected_fill: Option<Color32>) -> Self {
|
||||
Self {
|
||||
selected,
|
||||
selected_fill,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
|
||||
let Self { selected } = self;
|
||||
let Self {
|
||||
selected,
|
||||
selected_fill,
|
||||
} = self;
|
||||
|
||||
Frame::none()
|
||||
Frame::NONE
|
||||
.show(ui, |ui| {
|
||||
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
// take into account the stroke width
|
||||
let inner_margin = Margin::symmetric(
|
||||
ui.style().spacing.button_padding.x,
|
||||
ui.style().spacing.button_padding.y,
|
||||
ui.style().spacing.button_padding.x as i8 - 1,
|
||||
ui.style().spacing.button_padding.y as i8 - 1,
|
||||
);
|
||||
|
||||
if selected
|
||||
|| response.hovered()
|
||||
|| response.highlighted()
|
||||
|| response.has_focus()
|
||||
{
|
||||
// since the stroke is drawn inside the frame, we always reserve space for it
|
||||
if selected && response.hovered() {
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::none()
|
||||
.stroke(visuals.bg_stroke)
|
||||
.rounding(visuals.rounding)
|
||||
Frame::NONE
|
||||
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(selected_fill.unwrap_or(visuals.bg_fill))
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else if response.hovered() || response.highlighted() || response.has_focus() {
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::NONE
|
||||
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(visuals.bg_fill)
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else if selected {
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::NONE
|
||||
.stroke(Stroke::new(1.0, visuals.bg_fill))
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(selected_fill.unwrap_or(visuals.bg_fill))
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else {
|
||||
Frame::none()
|
||||
Frame::NONE
|
||||
.stroke(Stroke::new(1.0, Color32::TRANSPARENT))
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
}
|
||||
@@ -50,6 +84,6 @@ impl SelectableFrame {
|
||||
response
|
||||
})
|
||||
.inner
|
||||
.on_hover_cursor(eframe::egui::CursorIcon::PointingHand)
|
||||
.on_hover_cursor(CursorIcon::PointingHand)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Disks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StorageConfig {
|
||||
/// Enable the Storage widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<StorageConfig> for Storage {
|
||||
fn from(value: StorageConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
pub enable: bool,
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn output(&mut self) -> Vec<String> {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.disks.refresh();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
|
||||
disks.push(match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
})
|
||||
}
|
||||
|
||||
disks.sort();
|
||||
disks.reverse();
|
||||
|
||||
disks
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
for output in self.output() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::HARD_DRIVES.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TimeConfig {
|
||||
/// Enable the Time widget
|
||||
pub enable: bool,
|
||||
/// Set the Time format
|
||||
pub format: TimeFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<TimeConfig> for Time {
|
||||
fn from(value: TimeConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum TimeFormat {
|
||||
/// Twelve-hour format (with seconds)
|
||||
TwelveHour,
|
||||
/// Twenty-four-hour format (with seconds)
|
||||
TwentyFourHour,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl TimeFormat {
|
||||
pub fn toggle(&mut self) {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
|
||||
TimeFormat::TwentyFourHour => String::from("%T"),
|
||||
TimeFormat::Custom(format) => format.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Time {
|
||||
pub enable: bool,
|
||||
pub format: TimeFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CLOCK.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use crate::battery::Battery;
|
||||
use crate::battery::BatteryConfig;
|
||||
use crate::cpu::Cpu;
|
||||
use crate::cpu::CpuConfig;
|
||||
use crate::date::Date;
|
||||
use crate::date::DateConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiConfig;
|
||||
use crate::media::Media;
|
||||
use crate::media::MediaConfig;
|
||||
use crate::memory::Memory;
|
||||
use crate::memory::MemoryConfig;
|
||||
use crate::network::Network;
|
||||
use crate::network::NetworkConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::storage::Storage;
|
||||
use crate::storage::StorageConfig;
|
||||
use crate::time::Time;
|
||||
use crate::time::TimeConfig;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub trait BarWidget {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum WidgetConfig {
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
Komorebi(KomorebiConfig),
|
||||
Media(MediaConfig),
|
||||
Memory(MemoryConfig),
|
||||
Network(NetworkConfig),
|
||||
Storage(StorageConfig),
|
||||
Time(TimeConfig),
|
||||
}
|
||||
|
||||
impl WidgetConfig {
|
||||
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
|
||||
match self {
|
||||
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::Komorebi(config) => Box::new(Komorebi::from(config)),
|
||||
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
||||
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
|
||||
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
191
komorebi-bar/src/widgets/battery.rs
Normal file
191
komorebi-bar/src/widgets/battery.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::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 serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use starship_battery::Manager;
|
||||
use starship_battery::State;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct BatteryConfig {
|
||||
/// Enable the Battery widget
|
||||
pub enable: bool,
|
||||
/// Hide the widget if the battery is at full charge
|
||||
pub hide_on_full_charge: Option<bool>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is under this value [[1-100]]
|
||||
pub auto_select_under: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
fn from(value: BatteryConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
|
||||
manager: Manager::new().unwrap(),
|
||||
last_state: None,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
auto_select_under: value.auto_select_under.map(|u| u.clamp(1, 100)),
|
||||
state: BatteryState::Discharging,
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BatteryState {
|
||||
Charging,
|
||||
Discharging,
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BatteryOutput {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Battery {
|
||||
pub enable: bool,
|
||||
hide_on_full_charge: bool,
|
||||
manager: Manager,
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_under: Option<u8>,
|
||||
last_state: Option<BatteryOutput>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Battery {
|
||||
fn output(&mut self) -> Option<BatteryOutput> {
|
||||
let mut output = self.last_state.clone();
|
||||
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
output = None;
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||
|
||||
if percentage == 100 && self.hide_on_full_charge {
|
||||
output = None
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => {
|
||||
self.state = match percentage {
|
||||
p if p > 75 => BatteryState::Discharging,
|
||||
p if p > 50 => BatteryState::High,
|
||||
p if p > 25 => BatteryState::Medium,
|
||||
p if p > 10 => BatteryState::Low,
|
||||
_ => BatteryState::Warning,
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
|
||||
|
||||
output = Some(BatteryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => {
|
||||
format!("{percentage}%")
|
||||
}
|
||||
},
|
||||
selected,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if let Some(output) = output {
|
||||
let emoji = match self.state {
|
||||
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
|
||||
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
|
||||
BatteryState::High => egui_phosphor::regular::BATTERY_HIGH,
|
||||
BatteryState::Medium => egui_phosphor::regular::BATTERY_MEDIUM,
|
||||
BatteryState::Low => egui_phosphor::regular::BATTERY_LOW,
|
||||
BatteryState::Warning => egui_phosphor::regular::BATTERY_WARNING,
|
||||
};
|
||||
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -18,7 +16,8 @@ use std::time::Instant;
|
||||
use sysinfo::RefreshKind;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct CpuConfig {
|
||||
/// Enable the Cpu widget
|
||||
pub enable: bool,
|
||||
@@ -26,45 +25,61 @@ pub struct CpuConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<CpuConfig> for Cpu {
|
||||
fn from(value: CpuConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
|
||||
|
||||
system.refresh_cpu_usage();
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_memory().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CpuOutput {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Cpu {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_over: Option<u8>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
fn output(&mut self) -> String {
|
||||
fn output(&mut self) -> CpuOutput {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_cpu_usage();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let used = self.system.global_cpu_usage();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
|
||||
let used = self.system.global_cpu_usage() as u8;
|
||||
let selected = self.auto_select_over.is_some_and(|o| used >= o);
|
||||
|
||||
CpuOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,13 +88,8 @@ impl BarWidget for Cpu {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
if !output.label.is_empty() {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
@@ -88,24 +98,27 @@ impl BarWidget for Cpu {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
240
komorebi-bar/src/widgets/date.rs
Normal file
240
komorebi-bar/src/widgets/date.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
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 serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Custom format with additive modifiers for integer format specifiers
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::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 = 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)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
pub enable: bool,
|
||||
/// Set the Date format
|
||||
pub format: DateFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
|
||||
///
|
||||
/// Use a custom format to display additional information, i.e.:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "Date": {
|
||||
/// "enable": true,
|
||||
/// "format": { "Custom": "%D %Z (Tokyo)" },
|
||||
/// "timezone": "Asia/Tokyo"
|
||||
/// }
|
||||
///}
|
||||
/// ```
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
impl From<DateConfig> for Date {
|
||||
fn from(value: DateConfig) -> Self {
|
||||
let data_refresh_interval = 1;
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
timezone: value.timezone,
|
||||
data_refresh_interval,
|
||||
last_state: String::new(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DateFormat {
|
||||
/// Month/Date/Year format (09/08/24)
|
||||
MonthDateYear,
|
||||
/// Year-Month-Date format (2024-09-08)
|
||||
YearMonthDate,
|
||||
/// Date-Month-Year format (8-Sep-2024)
|
||||
DateMonthYear,
|
||||
/// Day Date Month Year format (8 September 2024)
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
/// Custom format with modifiers
|
||||
CustomModifiers(CustomModifiers),
|
||||
}
|
||||
|
||||
impl DateFormat {
|
||||
pub fn next(&mut self) {
|
||||
match self {
|
||||
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
|
||||
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
|
||||
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
|
||||
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Date {
|
||||
pub enable: bool,
|
||||
pub format: DateFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
timezone: Option<String>,
|
||||
data_refresh_interval: u64,
|
||||
last_state: String,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn output(&mut self) -> String {
|
||||
let mut output = self.last_state.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
let formatted = match &self.timezone {
|
||||
Some(timezone) => match timezone.parse::<Tz>() {
|
||||
Ok(tz) => Local::now()
|
||||
.with_timezone(&tz)
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Err(_) => format!("Invalid timezone: {}", timezone),
|
||||
},
|
||||
None => Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
// if custom modifiers are used, apply them
|
||||
output = match &self.format {
|
||||
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
|
||||
_ => formatted,
|
||||
};
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Date {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CALENDAR_DOTS.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "DATE: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
177
komorebi-bar/src/widgets/keyboard.rs
Normal file
177
komorebi-bar/src/widgets/keyboard.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::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 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)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::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))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
984
komorebi-bar/src/widgets/komorebi.rs
Normal file
984
komorebi-bar/src/widgets/komorebi.rs
Normal file
@@ -0,0 +1,984 @@
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::WorkspacesDisplayFormat;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widgets::komorebi_layout::KomorebiLayout;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::ICON_CACHE;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_INDEX;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::RichText;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use eframe::egui::Ui;
|
||||
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 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;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiConfig {
|
||||
/// Configure the Workspaces widget
|
||||
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 Container widget
|
||||
#[serde(alias = "focused_window")]
|
||||
pub focused_container: Option<KomorebiFocusedContainerConfig>,
|
||||
/// Configure the Locked Container widget
|
||||
pub locked_container: Option<KomorebiLockedContainerConfig>,
|
||||
/// Configure the Configuration Switcher widget
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiWorkspacesConfig {
|
||||
/// Enable the Komorebi Workspaces widget
|
||||
pub enable: bool,
|
||||
/// Hide workspaces without any windows
|
||||
pub hide_empty_workspaces: bool,
|
||||
/// Display format of the workspace
|
||||
pub display: Option<WorkspacesDisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiLayoutConfig {
|
||||
/// Enable the Komorebi Layout widget
|
||||
pub enable: bool,
|
||||
/// List of layout options
|
||||
pub options: Option<Vec<KomorebiLayout>>,
|
||||
/// Display format of the current layout
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiWorkspaceLayerConfig {
|
||||
/// Enable the Komorebi Workspace Layer widget
|
||||
pub enable: bool,
|
||||
/// Display format of the current layer
|
||||
pub display: Option<DisplayFormat>,
|
||||
/// Show the widget event if the layer is Tiling
|
||||
pub show_when_tiling: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiFocusedContainerConfig {
|
||||
/// Enable the Komorebi Focused Container widget
|
||||
pub enable: bool,
|
||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused container)
|
||||
pub show_icon: Option<bool>,
|
||||
/// Display format of the currently focused container
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiLockedContainerConfig {
|
||||
/// Enable the Komorebi Locked Container widget
|
||||
pub enable: bool,
|
||||
/// Display format of the current locked state
|
||||
pub display: Option<DisplayFormat>,
|
||||
/// Show the widget event if the layer is unlocked
|
||||
pub show_when_unlocked: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiConfigurationSwitcherConfig {
|
||||
/// Enable the Komorebi Configurations widget
|
||||
pub enable: bool,
|
||||
/// A map of display friendly name => path to configuration.json
|
||||
pub configurations: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl From<&KomorebiConfig> for Komorebi {
|
||||
fn from(value: &KomorebiConfig) -> Self {
|
||||
let configuration_switcher =
|
||||
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()).replace_env())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
Some(configuration_switcher)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
|
||||
selected_workspace: String::new(),
|
||||
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
workspaces: vec![],
|
||||
hide_empty_workspaces: value
|
||||
.workspaces
|
||||
.map(|w| w.hide_empty_workspaces)
|
||||
.unwrap_or_default(),
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: (
|
||||
false,
|
||||
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_container: value.focused_container,
|
||||
workspace_layer: value.workspace_layer,
|
||||
locked_container: value.locked_container,
|
||||
configuration_switcher,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Komorebi {
|
||||
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
pub focused_container: Option<KomorebiFocusedContainerConfig>,
|
||||
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
|
||||
pub locked_container: Option<KomorebiLockedContainerConfig>,
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
impl BarWidget for Komorebi {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
|
||||
let icon_size = Vec2::splat(config.icon_font_id.size);
|
||||
let text_size = Vec2::splat(config.text_font_id.size);
|
||||
|
||||
if let Some(workspaces) = self.workspaces {
|
||||
if workspaces.enable {
|
||||
let mut update = None;
|
||||
|
||||
if !komorebi_notification_state.workspaces.is_empty() {
|
||||
let format = workspaces.display.unwrap_or(DisplayFormat::Text.into());
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, containers, _, should_show)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
if *should_show {
|
||||
let is_selected = komorebi_notification_state.selected_workspace.eq(ws);
|
||||
|
||||
if SelectableFrame::new(
|
||||
is_selected,
|
||||
)
|
||||
.show(ui, |ui| {
|
||||
let mut has_icon = false;
|
||||
|
||||
if format == WorkspacesDisplayFormat::AllIcons
|
||||
|| format == WorkspacesDisplayFormat::AllIconsAndText
|
||||
|| format == WorkspacesDisplayFormat::AllIconsAndTextOnSelected
|
||||
|| format == DisplayFormat::Icon.into()
|
||||
|| format == DisplayFormat::IconAndText.into()
|
||||
|| format == DisplayFormat::IconAndTextOnSelected.into()
|
||||
|| (format == DisplayFormat::TextAndIconOnSelected.into() && is_selected)
|
||||
{
|
||||
has_icon = containers.iter().any(|(_, container_info)| {
|
||||
container_info.icons.iter().any(|icon| icon.is_some())
|
||||
});
|
||||
|
||||
if has_icon {
|
||||
Frame::NONE
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y as i8,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
for (is_focused, container) in containers {
|
||||
for icon in container.icons.iter().flatten().collect::<Vec<_>>() {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, icon))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(if *is_focused { icon_size } else { text_size }),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// draw a custom icon when there is no app icon or text
|
||||
if !has_icon && (matches!(format, WorkspacesDisplayFormat::AllIcons | WorkspacesDisplayFormat::Existing(DisplayFormat::Icon))
|
||||
|| (!is_selected && matches!(format, WorkspacesDisplayFormat::AllIconsAndTextOnSelected | WorkspacesDisplayFormat::Existing(DisplayFormat::IconAndTextOnSelected)))) {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(icon_size, Sense::hover());
|
||||
let stroke = Stroke::new(
|
||||
1.0,
|
||||
if is_selected { ctx.style().visuals.selection.stroke.color} else { ui.style().visuals.text_color() },
|
||||
);
|
||||
let mut rect = response.rect;
|
||||
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
|
||||
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
|
||||
|
||||
response.on_hover_text(ws.to_string())
|
||||
// add hover text when there are only icons
|
||||
} else if match format {
|
||||
WorkspacesDisplayFormat::AllIcons | WorkspacesDisplayFormat::Existing(DisplayFormat::Icon) => has_icon,
|
||||
_ => false,
|
||||
} {
|
||||
ui.response().on_hover_text(ws.to_string())
|
||||
// add label only
|
||||
} else if (format != WorkspacesDisplayFormat::AllIconsAndTextOnSelected && format != DisplayFormat::IconAndTextOnSelected.into())
|
||||
|| (is_selected && matches!(format, WorkspacesDisplayFormat::AllIconsAndTextOnSelected | WorkspacesDisplayFormat::Existing(DisplayFormat::IconAndTextOnSelected)))
|
||||
{
|
||||
if is_selected {
|
||||
ui.add(Label::new(RichText::new(ws.to_string()).color(ctx.style().visuals.selection.stroke.color)).selectable(false))
|
||||
}
|
||||
else {
|
||||
ui.add(Label::new(ws.to_string()).selectable(false))
|
||||
}
|
||||
} else {
|
||||
ui.response()
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
|
||||
if komorebi_notification_state.mouse_follows_focus {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::MouseFollowsFocus(false),
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
SocketMessage::RetileWithResizeDimensions,
|
||||
SocketMessage::MouseFollowsFocus(true),
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
MouseFollowsFocus(false)\n
|
||||
FocusMonitorWorkspaceNumber({}, {})\n
|
||||
RetileWithResizeDimensions
|
||||
MouseFollowsFocus(true)\n",
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
);
|
||||
}
|
||||
} else if komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
SocketMessage::RetileWithResizeDimensions,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
FocusMonitorWorkspaceNumber({}, {})\n
|
||||
RetileWithResizeDimensions",
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(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 {
|
||||
if (layer_config.show_when_tiling.unwrap_or_default()
|
||||
&& matches!(layer, WorkspaceLayer::Tiling))
|
||||
|| matches!(layer, WorkspaceLayer::Floating)
|
||||
{
|
||||
let display_format = layer_config.display.unwrap_or(DisplayFormat::Text);
|
||||
let size = Vec2::splat(config.icon_font_id.size);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
let layer_frame = SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if display_format != DisplayFormat::Text {
|
||||
if matches!(layer, WorkspaceLayer::Tiling) {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(size, Sense::hover());
|
||||
let color = ctx.style().visuals.selection.stroke.color;
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let corner =
|
||||
CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
|
||||
// tiling
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.48);
|
||||
rect_left.set_height(rect.height() * 0.98);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.01 + stroke.width,
|
||||
rect.width() * 0.01 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.51 + stroke.width,
|
||||
rect.width() * 0.01 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, corner, color);
|
||||
painter.rect_stroke(
|
||||
rect_right,
|
||||
corner,
|
||||
stroke,
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
} else {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(size, Sense::hover());
|
||||
let color = ctx.style().visuals.selection.stroke.color;
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let corner =
|
||||
CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
|
||||
// floating
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.65);
|
||||
rect_left.set_height(rect.height() * 0.65);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.01 + stroke.width,
|
||||
rect.width() * 0.01 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.34 + stroke.width,
|
||||
rect.width() * 0.34 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, corner, color);
|
||||
painter.rect_stroke(
|
||||
rect_right,
|
||||
corner,
|
||||
stroke,
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if display_format != DisplayFormat::Icon {
|
||||
ui.add(Label::new(layer.to_string()).selectable(false));
|
||||
}
|
||||
})
|
||||
.on_hover_text(layer.to_string());
|
||||
|
||||
if layer_frame.clicked()
|
||||
&& komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
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
|
||||
.workspaces
|
||||
.iter()
|
||||
.position(|o| komorebi_notification_state.selected_workspace.eq(&o.0));
|
||||
|
||||
komorebi_notification_state.layout.show(
|
||||
ctx,
|
||||
ui,
|
||||
config,
|
||||
layout_config,
|
||||
workspace_idx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(configuration_switcher) = &self.configuration_switcher {
|
||||
if configuration_switcher.enable {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file() {
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(name).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
let canonicalized =
|
||||
dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::ReplaceConfiguration(canonicalized),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(locked_container_config) = self.locked_container {
|
||||
if locked_container_config.enable {
|
||||
let is_locked = komorebi_notification_state.focused_container_information.0;
|
||||
|
||||
if locked_container_config
|
||||
.show_when_unlocked
|
||||
.unwrap_or_default()
|
||||
|| is_locked
|
||||
{
|
||||
let titles = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.1
|
||||
.titles;
|
||||
|
||||
if !titles.is_empty() {
|
||||
let display_format = locked_container_config
|
||||
.display
|
||||
.unwrap_or(DisplayFormat::Text);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
if display_format != DisplayFormat::Text {
|
||||
if is_locked {
|
||||
egui_phosphor::regular::LOCK_KEY.to_string()
|
||||
} else {
|
||||
egui_phosphor::regular::LOCK_SIMPLE_OPEN.to_string()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if display_format != DisplayFormat::Icon {
|
||||
layout_job.append(
|
||||
if is_locked { "Locked" } else { "Unlocked" },
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
&& komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
SocketMessage::ToggleLock,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send ToggleLock");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_container_config) = self.focused_container {
|
||||
if focused_container_config.enable {
|
||||
let titles = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.1
|
||||
.titles;
|
||||
|
||||
if !titles.is_empty() {
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
let icons = &komorebi_notification_state
|
||||
.focused_container_information.1
|
||||
.icons;
|
||||
let focused_window_idx = komorebi_notification_state
|
||||
.focused_container_information.1
|
||||
.focused_window_idx;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
let len = iter.len();
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
let selected = i == focused_window_idx && len != 1;
|
||||
let text_color = if selected { ctx.style().visuals.selection.stroke.color } else { ui.style().visuals.text_color() };
|
||||
|
||||
if SelectableFrame::new(selected)
|
||||
.show(ui, |ui| {
|
||||
// handle legacy setting
|
||||
let format = focused_container_config.display.unwrap_or(
|
||||
if focused_container_config.show_icon.unwrap_or(false) {
|
||||
DisplayFormat::IconAndText
|
||||
} else {
|
||||
DisplayFormat::Text
|
||||
},
|
||||
);
|
||||
|
||||
if format == DisplayFormat::Icon
|
||||
|| format == DisplayFormat::IconAndText
|
||||
|| format == DisplayFormat::IconAndTextOnSelected
|
||||
|| (format == DisplayFormat::TextAndIconOnSelected
|
||||
&& i == focused_window_idx)
|
||||
{
|
||||
if let Some(img) = icon {
|
||||
Frame::NONE
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y as i8,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
let response = ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(icon_size),
|
||||
);
|
||||
|
||||
if let DisplayFormat::Icon = format {
|
||||
response.on_hover_text(title);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if format == DisplayFormat::Text
|
||||
|| format == DisplayFormat::IconAndText
|
||||
|| format == DisplayFormat::TextAndIconOnSelected
|
||||
|| (format == DisplayFormat::IconAndTextOnSelected
|
||||
&& i == focused_window_idx)
|
||||
{
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(RichText::new( title).color(text_color)).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
if selected {
|
||||
return;
|
||||
}
|
||||
|
||||
if komorebi_notification_state.mouse_follows_focus {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::MouseFollowsFocus(false),
|
||||
SocketMessage::FocusStackWindow(i),
|
||||
SocketMessage::MouseFollowsFocus(true),
|
||||
]).is_err() {
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
MouseFollowsFocus(false)\n
|
||||
FocusStackWindow({})\n
|
||||
MouseFollowsFocus(true)\n",
|
||||
i,
|
||||
);
|
||||
}
|
||||
} else if komorebi_client::send_message(
|
||||
&SocketMessage::FocusStackWindow(i)
|
||||
).is_err() {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
|
||||
ctx.load_texture("icon", color_image, TextureOptions::default())
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<(
|
||||
String,
|
||||
Vec<(bool, KomorebiNotificationStateContainerInformation)>,
|
||||
WorkspaceLayer,
|
||||
bool,
|
||||
)>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: (bool, KomorebiNotificationStateContainerInformation),
|
||||
pub layout: KomorebiLayout,
|
||||
pub hide_empty_workspaces: bool,
|
||||
pub mouse_follows_focus: bool,
|
||||
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 {
|
||||
pub fn update_from_config(&mut self, config: &Self) {
|
||||
self.hide_empty_workspaces = config.hide_empty_workspaces;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_notification(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
monitor_index: Option<usize>,
|
||||
notification: komorebi_client::Notification,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
grouping: Option<Grouping>,
|
||||
default_theme: Option<KomobarTheme>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
let show_all_icons = render_config.borrow().show_all_icons;
|
||||
|
||||
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.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("removed theme from updated komorebi.json and applied default theme");
|
||||
} else {
|
||||
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(*theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
if show_all_icons {
|
||||
let mut containers = vec![];
|
||||
let mut has_monocle = false;
|
||||
|
||||
// add monocle container
|
||||
if let Some(container) = ws.monocle_container() {
|
||||
containers.push((true, container.into()));
|
||||
has_monocle = true;
|
||||
}
|
||||
|
||||
// add all tiled windows
|
||||
for (i, container) in ws.containers().iter().enumerate() {
|
||||
containers.push((
|
||||
!has_monocle && i == ws.focused_container_idx(),
|
||||
container.into(),
|
||||
));
|
||||
}
|
||||
|
||||
// add all floating windows
|
||||
for floating_window in ws.floating_windows() {
|
||||
containers.push((
|
||||
!has_monocle && floating_window.is_focused(),
|
||||
floating_window.into(),
|
||||
));
|
||||
}
|
||||
|
||||
containers
|
||||
} else {
|
||||
vec![(true, ws.into())]
|
||||
},
|
||||
ws.layer().to_owned(),
|
||||
should_show,
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
let focused_workspace = &monitor.workspaces()[focused_workspace_idx];
|
||||
let is_focused = focused_workspace
|
||||
.locked_containers()
|
||||
.contains(&focused_workspace.focused_container_idx());
|
||||
|
||||
self.focused_container_information = (is_focused, focused_workspace.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationStateContainerInformation {
|
||||
pub titles: Vec<String>,
|
||||
pub icons: Vec<Option<RgbaImage>>,
|
||||
pub focused_window_idx: usize,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Workspace) -> Self {
|
||||
let mut container_info = Self::EMPTY;
|
||||
|
||||
if let Some(container) = value.monocle_container() {
|
||||
container_info = container.into();
|
||||
} else if let Some(container) = value.focused_container() {
|
||||
container_info = container.into();
|
||||
}
|
||||
|
||||
for floating_window in value.floating_windows() {
|
||||
if floating_window.is_focused() {
|
||||
container_info = floating_window.into();
|
||||
}
|
||||
}
|
||||
|
||||
container_info
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Container) -> Self {
|
||||
let windows = value.windows().iter().collect::<Vec<_>>();
|
||||
let mut icons = vec![];
|
||||
|
||||
for window in windows {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let hwnd = window.hwnd;
|
||||
|
||||
match icon_cache.get(&hwnd) {
|
||||
None => {
|
||||
let icon = match windows_icons::get_icon_by_hwnd(window.hwnd) {
|
||||
None => windows_icons_fallback::get_icon_by_process_id(window.process_id()),
|
||||
Some(icon) => Some(icon),
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
icons.push(Some(icon.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(hwnd, icon.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
titles: value
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
icons,
|
||||
focused_window_idx: value.focused_window_idx(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Window) -> Self {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let mut icons = vec![];
|
||||
let hwnd = value.hwnd;
|
||||
|
||||
match icon_cache.get(&hwnd) {
|
||||
None => {
|
||||
let icon = match windows_icons::get_icon_by_hwnd(hwnd) {
|
||||
None => windows_icons_fallback::get_icon_by_process_id(value.process_id()),
|
||||
Some(icon) => Some(icon),
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
icons.push(Some(icon.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(hwnd, icon.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
titles: vec![value.title().unwrap_or_default()],
|
||||
icons,
|
||||
focused_window_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiNotificationStateContainerInformation {
|
||||
pub const EMPTY: Self = Self {
|
||||
titles: vec![],
|
||||
icons: vec![],
|
||||
focused_window_idx: 0,
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::komorebi::KomorebiLayoutConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::komorebi::KomorebiLayoutConfig;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::SocketMessage;
|
||||
use schemars::JsonSchema;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
@@ -23,7 +22,8 @@ use serde_json::from_str;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum KomorebiLayout {
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
@@ -105,12 +105,22 @@ impl KomorebiLayout {
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
SocketMessage::ToggleMonocle,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: ToggleMonocle");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Floating => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
SocketMessage::ToggleTiling,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: ToggleTiling");
|
||||
}
|
||||
}
|
||||
@@ -123,18 +133,22 @@ impl KomorebiLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_icon(&mut self, font_id: FontId, ctx: &Context, ui: &mut Ui) {
|
||||
fn show_icon(&mut self, is_selected: bool, font_id: FontId, ctx: &Context, ui: &mut Ui) {
|
||||
// paint custom icons for the layout
|
||||
let size = Vec2::splat(font_id.size);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let color = ctx.style().visuals.selection.stroke.color;
|
||||
let color = if is_selected {
|
||||
ctx.style().visuals.selection.stroke.color
|
||||
} else {
|
||||
ui.style().visuals.text_color()
|
||||
};
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let rounding = Rounding::same(rect.width() * 0.1);
|
||||
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke);
|
||||
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
|
||||
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => match layout {
|
||||
@@ -190,7 +204,7 @@ impl KomorebiLayout {
|
||||
rect.width() * 0.35 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, rounding, color);
|
||||
painter.rect_stroke(rect_right, rounding, stroke);
|
||||
painter.rect_stroke(rect_right, rounding, stroke, StrokeKind::Outside);
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
let mut rect_left = response.rect;
|
||||
@@ -225,13 +239,7 @@ impl KomorebiLayout {
|
||||
workspace_idx: Option<usize>,
|
||||
) {
|
||||
let monitor_idx = render_config.monitor_idx;
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let font_id = render_config.icon_font_id.clone();
|
||||
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
|
||||
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
|
||||
|
||||
@@ -243,7 +251,7 @@ impl KomorebiLayout {
|
||||
let layout_frame = SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
self.show_icon(font_id.clone(), ctx, ui);
|
||||
self.show_icon(true, font_id.clone(), ctx, ui);
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
|
||||
@@ -258,7 +266,7 @@ impl KomorebiLayout {
|
||||
|
||||
if show_options {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
Frame::none().show(ui, |ui| {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
@@ -286,8 +294,12 @@ impl KomorebiLayout {
|
||||
]);
|
||||
|
||||
for layout_option in &mut layout_options {
|
||||
if SelectableFrame::new(self == layout_option)
|
||||
.show(ui, |ui| layout_option.show_icon(font_id.clone(), ctx, ui))
|
||||
let is_selected = self == layout_option;
|
||||
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
@@ -1,23 +1,22 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct MediaConfig {
|
||||
/// Enable the Media widget
|
||||
pub enable: bool,
|
||||
@@ -82,16 +81,9 @@ impl BarWidget for Media {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::HEADPHONES.to_string(),
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -99,24 +91,28 @@ impl BarWidget for Media {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job).selectable(false).truncate(),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
@@ -1,15 +1,13 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -18,7 +16,8 @@ use std::time::Instant;
|
||||
use sysinfo::RefreshKind;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct MemoryConfig {
|
||||
/// Enable the Memory widget
|
||||
pub enable: bool,
|
||||
@@ -26,35 +25,46 @@ pub struct MemoryConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<MemoryConfig> for Memory {
|
||||
fn from(value: MemoryConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
|
||||
|
||||
system.refresh_memory();
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_cpu().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct MemoryOutput {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Memory {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_over: Option<u8>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
fn output(&mut self) -> String {
|
||||
fn output(&mut self) -> MemoryOutput {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_memory();
|
||||
@@ -63,11 +73,17 @@ impl Memory {
|
||||
|
||||
let used = self.system.used_memory();
|
||||
let total = self.system.total_memory();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
let usage = ((used * 100) / total) as u8;
|
||||
let selected = self.auto_select_over.is_some_and(|o| usage >= o);
|
||||
|
||||
MemoryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", usage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,13 +92,8 @@ impl BarWidget for Memory {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
if !output.label.is_empty() {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
@@ -91,24 +102,27 @@ impl BarWidget for Memory {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
13
komorebi-bar/src/widgets/mod.rs
Normal file
13
komorebi-bar/src/widgets/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod battery;
|
||||
pub mod cpu;
|
||||
pub mod date;
|
||||
pub mod keyboard;
|
||||
pub mod komorebi;
|
||||
mod komorebi_layout;
|
||||
pub mod media;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod storage;
|
||||
pub mod time;
|
||||
pub mod update;
|
||||
pub mod widget;
|
||||
540
komorebi-bar/src/widgets/network.rs
Normal file
540
komorebi-bar/src/widgets/network.rs
Normal file
@@ -0,0 +1,540 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use num_derive::FromPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
/// Show total received and transmitted activity
|
||||
#[serde(alias = "show_total_data_transmitted")]
|
||||
pub show_total_activity: bool,
|
||||
/// Show received and transmitted activity
|
||||
#[serde(alias = "show_network_activity")]
|
||||
pub show_activity: bool,
|
||||
/// Show default interface
|
||||
pub show_default_interface: Option<bool>,
|
||||
/// Characters to reserve for received and transmitted activity
|
||||
#[serde(alias = "network_activity_fill_characters")]
|
||||
pub activity_left_padding: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
|
||||
pub auto_select: Option<NetworkSelectConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct NetworkSelectConfig {
|
||||
/// Select the total received data when it's over this value
|
||||
pub total_received_over: Option<u64>,
|
||||
/// Select the total transmitted data when it's over this value
|
||||
pub total_transmitted_over: Option<u64>,
|
||||
/// Select the received data when it's over this value
|
||||
pub received_over: Option<u64>,
|
||||
/// Select the transmitted data when it's over this value
|
||||
pub transmitted_over: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
show_total_activity: value.show_total_activity,
|
||||
show_activity: value.show_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
auto_select: value.auto_select,
|
||||
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
|
||||
last_state_total_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select: Option<NetworkSelectConfig>,
|
||||
default_interface: String,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
activity_left_padding: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh(true);
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
let received = Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
let transmitted = Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
ReadingValue::from(received),
|
||||
ReadingValue::from(transmitted),
|
||||
));
|
||||
}
|
||||
|
||||
if self.show_total_activity {
|
||||
let total_received =
|
||||
Self::to_pretty_bytes(data.total_received(), 1);
|
||||
let total_transmitted =
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1);
|
||||
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
ReadingValue::from(total_received),
|
||||
ReadingValue::from(total_transmitted),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn reading_to_labels(
|
||||
&self,
|
||||
select_received: bool,
|
||||
select_transmitted: bool,
|
||||
ctx: &Context,
|
||||
reading: &NetworkReading,
|
||||
config: RenderConfig,
|
||||
) -> (Label, Label) {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s ",
|
||||
reading.received.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} ", reading.received.pretty),
|
||||
reading.transmitted.pretty.clone(),
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s ",
|
||||
reading.received.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s ", reading.received.pretty),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted.pretty),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let auto_text_color_received = config.auto_select_text.filter(|_| select_received);
|
||||
let auto_text_color_transmitted = config.auto_select_text.filter(|_| select_transmitted);
|
||||
|
||||
// icon
|
||||
let mut layout_job_down = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
if select_received {
|
||||
egui_phosphor::regular::ARROW_FAT_LINES_DOWN.to_string()
|
||||
} else {
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
|
||||
}
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color_received.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job_down.append(
|
||||
&text_down,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color_received.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// icon
|
||||
let mut layout_job_up = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
if select_transmitted {
|
||||
egui_phosphor::regular::ARROW_FAT_LINES_UP.to_string()
|
||||
} else {
|
||||
egui_phosphor::regular::ARROW_FAT_UP.to_string()
|
||||
}
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color_transmitted.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job_up.append(
|
||||
&text_up,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color_transmitted.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
(
|
||||
Label::new(layout_job_down).selectable(false),
|
||||
Label::new(layout_job_up).selectable(false),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> (u64, String) {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
(
|
||||
input as u64,
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn show_frame<R>(
|
||||
&self,
|
||||
selected: bool,
|
||||
auto_focus_fill: Option<Color32>,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) {
|
||||
if SelectableFrame::new_auto(selected, auto_focus_fill)
|
||||
.show(ui, add_contents)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||
|
||||
// widget spacing: make sure to use the same config to call the apply_on_widget function
|
||||
let mut render_config = config.clone();
|
||||
|
||||
if self.show_total_activity || self.show_activity {
|
||||
let (activity, total_activity) = self.network_activity();
|
||||
|
||||
if self.show_total_activity {
|
||||
for reading in &total_activity {
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
let select_received = self.auto_select.is_some_and(|f| {
|
||||
f.total_received_over
|
||||
.is_some_and(|o| reading.received.value > o)
|
||||
});
|
||||
let select_transmitted = self.auto_select.is_some_and(|f| {
|
||||
f.total_transmitted_over
|
||||
.is_some_and(|o| reading.transmitted.value > o)
|
||||
});
|
||||
|
||||
let labels = self.reading_to_labels(
|
||||
select_received,
|
||||
select_transmitted,
|
||||
ctx,
|
||||
reading,
|
||||
config.clone(),
|
||||
);
|
||||
|
||||
if is_reversed {
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
} else {
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_activity {
|
||||
for reading in &activity {
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
let select_received = self.auto_select.is_some_and(|f| {
|
||||
f.received_over.is_some_and(|o| reading.received.value > o)
|
||||
});
|
||||
let select_transmitted = self.auto_select.is_some_and(|f| {
|
||||
f.transmitted_over
|
||||
.is_some_and(|o| reading.transmitted.value > o)
|
||||
});
|
||||
|
||||
let labels = self.reading_to_labels(
|
||||
select_received,
|
||||
select_transmitted,
|
||||
ctx,
|
||||
reading,
|
||||
config.clone(),
|
||||
);
|
||||
|
||||
if is_reversed {
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
} else {
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_default_interface {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
self.show_frame(false, None, ui, |ui| {
|
||||
ui.add(Label::new(layout_job).selectable(false))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// widget spacing: pass on the config that was use for calling the apply_on_widget function
|
||||
*config = render_config.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum NetworkReadingFormat {
|
||||
Speed = 0,
|
||||
Total = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReadingValue {
|
||||
value: u64,
|
||||
pretty: String,
|
||||
}
|
||||
|
||||
impl From<(u64, String)> for ReadingValue {
|
||||
fn from(value: (u64, String)) -> Self {
|
||||
Self {
|
||||
value: value.0,
|
||||
pretty: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NetworkReading {
|
||||
format: NetworkReadingFormat,
|
||||
received: ReadingValue,
|
||||
transmitted: ReadingValue,
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
fn new(
|
||||
format: NetworkReadingFormat,
|
||||
received: ReadingValue,
|
||||
transmitted: ReadingValue,
|
||||
) -> Self {
|
||||
Self {
|
||||
format,
|
||||
received,
|
||||
transmitted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPrimitive)]
|
||||
enum DataUnit {
|
||||
B = 0,
|
||||
K = 1,
|
||||
M = 2,
|
||||
G = 3,
|
||||
T = 4,
|
||||
P = 5,
|
||||
E = 6,
|
||||
Z = 7,
|
||||
Y = 8,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
161
komorebi-bar/src/widgets/storage.rs
Normal file
161
komorebi-bar/src/widgets/storage.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::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 serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Disks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct StorageConfig {
|
||||
/// Enable the Storage widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
/// Hide when the current percentage is under this value [[1-100]]
|
||||
pub auto_hide_under: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<StorageConfig> for Storage {
|
||||
fn from(value: StorageConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StorageDisk {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
pub enable: bool,
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_over: Option<u8>,
|
||||
auto_hide_under: Option<u8>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn output(&mut self) -> Vec<StorageDisk> {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.disks.refresh(true);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
let percentage = ((used * 100) / total) as u8;
|
||||
|
||||
let hide = self.auto_hide_under.is_some_and(|u| percentage <= u);
|
||||
|
||||
if !hide {
|
||||
let selected = self.auto_select_over.is_some_and(|o| percentage >= o);
|
||||
|
||||
disks.push(StorageDisk {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
|
||||
},
|
||||
selected,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
disks.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
|
||||
disks
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||
|
||||
if is_reversed {
|
||||
output.reverse();
|
||||
}
|
||||
|
||||
for output in output {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::HARD_DRIVES.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.label.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
538
komorebi-bar/src/widgets/time.rs
Normal file
538
komorebi-bar/src/widgets/time.rs
Normal file
@@ -0,0 +1,538 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono::NaiveTime;
|
||||
use chrono_tz::Tz;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
lazy_static! {
|
||||
static ref TIME_RANGES: Vec<(&'static str, NaiveTime)> = {
|
||||
vec![
|
||||
(
|
||||
egui_phosphor::regular::MOON,
|
||||
NaiveTime::from_hms_opt(0, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::ALARM,
|
||||
NaiveTime::from_hms_opt(6, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::BREAD,
|
||||
NaiveTime::from_hms_opt(6, 1, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::BARBELL,
|
||||
NaiveTime::from_hms_opt(6, 30, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::COFFEE,
|
||||
NaiveTime::from_hms_opt(8, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::CLOCK,
|
||||
NaiveTime::from_hms_opt(8, 30, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::HAMBURGER,
|
||||
NaiveTime::from_hms_opt(12, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::CLOCK_AFTERNOON,
|
||||
NaiveTime::from_hms_opt(12, 30, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::FORK_KNIFE,
|
||||
NaiveTime::from_hms_opt(18, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::MOON_STARS,
|
||||
NaiveTime::from_hms_opt(18, 30, 0).expect("invalid"),
|
||||
),
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TimeConfig {
|
||||
/// Enable the Time widget
|
||||
pub enable: bool,
|
||||
/// Set the Time format
|
||||
pub format: TimeFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
|
||||
///
|
||||
/// Use a custom format to display additional information, i.e.:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "Time": {
|
||||
/// "enable": true,
|
||||
/// "format": { "Custom": "%T %Z (Tokyo)" },
|
||||
/// "timezone": "Asia/Tokyo"
|
||||
/// }
|
||||
///}
|
||||
/// ```
|
||||
pub timezone: Option<String>,
|
||||
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00. (default: false)
|
||||
pub changing_icon: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<TimeConfig> for Time {
|
||||
fn from(value: TimeConfig) -> Self {
|
||||
// using 1 second made the widget look "less accurate" and lagging (especially having multiple with seconds).
|
||||
// This is still better than getting an update every frame
|
||||
let data_refresh_interval = 500;
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
timezone: value.timezone,
|
||||
changing_icon: value.changing_icon.unwrap_or_default(),
|
||||
data_refresh_interval_millis: data_refresh_interval,
|
||||
last_state: TimeOutput::new(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_millis(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
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),
|
||||
}
|
||||
|
||||
impl TimeFormat {
|
||||
pub fn toggle(&mut self) {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwelveHourWithoutSeconds,
|
||||
TimeFormat::TwelveHourWithoutSeconds => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwentyFourHourWithoutSeconds,
|
||||
TimeFormat::TwentyFourHourWithoutSeconds => *self = TimeFormat::BinaryCircle,
|
||||
TimeFormat::BinaryCircle => *self = TimeFormat::BinaryRectangle,
|
||||
TimeFormat::BinaryRectangle => *self = TimeFormat::TwelveHour,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TimeOutput {
|
||||
label: String,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
impl TimeOutput {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
label: String::new(),
|
||||
icon: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Time {
|
||||
pub enable: bool,
|
||||
pub format: TimeFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
timezone: Option<String>,
|
||||
changing_icon: bool,
|
||||
data_refresh_interval_millis: u64,
|
||||
last_state: TimeOutput,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
fn output(&mut self) -> TimeOutput {
|
||||
let mut output = self.last_state.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated)
|
||||
> Duration::from_millis(self.data_refresh_interval_millis)
|
||||
{
|
||||
let (formatted, current_time) = match &self.timezone {
|
||||
Some(timezone) => match timezone.parse::<Tz>() {
|
||||
Ok(tz) => {
|
||||
let dt = Local::now().with_timezone(&tz);
|
||||
(
|
||||
dt.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
|
||||
},
|
||||
None => {
|
||||
let dt = Local::now();
|
||||
(
|
||||
dt.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if current_time.is_none() {
|
||||
return TimeOutput {
|
||||
label: formatted,
|
||||
icon: egui_phosphor::regular::WARNING_CIRCLE.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let current_range = match &self.changing_icon {
|
||||
true => TIME_RANGES
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|&(_, start)| current_time.unwrap() > *start)
|
||||
.cloned(),
|
||||
false => None,
|
||||
}
|
||||
.unwrap_or((egui_phosphor::regular::CLOCK, NaiveTime::default()));
|
||||
|
||||
output = TimeOutput {
|
||||
label: formatted,
|
||||
icon: current_range.0.to_string(),
|
||||
};
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
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 offset = height / 2.0 - height / 8.0;
|
||||
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), 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 + offset), r, color);
|
||||
} else {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 1.50 + offset), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 2 || number == 3 || number == 6 || number == 7 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r, color);
|
||||
} else {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 4 || number == 5 || number == 6 || number == 7 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r, color);
|
||||
} else if max_power > 2 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 8 || number == 9 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), r, color);
|
||||
} else if max_power > 3 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), 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 offset = height / 2.0 - height / 8.0;
|
||||
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), Sense::hover());
|
||||
let color = ctx.style().visuals.text_color();
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
|
||||
let round_all = CornerRadius::same((response.rect.width() * 0.1) as u8);
|
||||
let round_top = CornerRadius {
|
||||
nw: round_all.nw,
|
||||
ne: round_all.ne,
|
||||
..Default::default()
|
||||
};
|
||||
let round_none = CornerRadius::ZERO;
|
||||
let round_bottom = CornerRadius {
|
||||
sw: round_all.nw,
|
||||
se: round_all.ne,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if max_power == 2 {
|
||||
let mut rect = response
|
||||
.rect
|
||||
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
|
||||
rect.set_height(rect.height() - height * 2.0);
|
||||
rect = rect.translate(Vec2::new(0.0, height * 2.0 + offset));
|
||||
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
|
||||
} else if max_power == 3 {
|
||||
let mut rect = response
|
||||
.rect
|
||||
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
|
||||
rect.set_height(rect.height() - height);
|
||||
rect = rect.translate(Vec2::new(0.0, height + offset));
|
||||
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
|
||||
} else {
|
||||
let mut rect = response
|
||||
.rect
|
||||
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
|
||||
rect = rect.translate(Vec2::new(0.0, 0.0 + offset));
|
||||
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
|
||||
}
|
||||
|
||||
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(stroke.width, height * 3.0 + offset * 2.0)),
|
||||
round_bottom,
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 2 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(stroke.width, height * 2.0 + offset * 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(stroke.width, height * 2.0 + offset * 2.0)),
|
||||
round_bottom,
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 4 || number == 5 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(stroke.width, height * 1.0 + offset * 2.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(stroke.width, height * 1.0 + offset * 2.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(stroke.width, height + offset * 2.0)),
|
||||
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(stroke.width, 0.0 + offset * 2.0)),
|
||||
round_top,
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.label.is_empty() {
|
||||
let use_binary_circle = output.label.starts_with('c');
|
||||
let use_binary_rectangle = output.label.starts_with('r');
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => output.icon,
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.label.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
if !use_binary_circle && !use_binary_rectangle {
|
||||
layout_job.append(
|
||||
&output.label,
|
||||
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| {
|
||||
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.label.chars().rev().collect()
|
||||
} else {
|
||||
output.label
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
komorebi-bar/src/widgets/update.rs
Normal file
158
komorebi-bar/src/widgets/update.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::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 serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct UpdateConfig {
|
||||
/// Enable the Update widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 12 hours)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<UpdateConfig> for Update {
|
||||
fn from(value: UpdateConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(12);
|
||||
|
||||
let mut latest_version = String::new();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
if let Ok(response) = client
|
||||
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
|
||||
.header("User-Agent", "komorebi-bar-version-checker")
|
||||
.send()
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
if let Ok(release) =
|
||||
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
|
||||
{
|
||||
let trimmed = release.tag_name.trim_start_matches("v");
|
||||
latest_version = trimmed.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
data_refresh_interval,
|
||||
installed_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
latest_version,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Update {
|
||||
pub enable: bool,
|
||||
data_refresh_interval: u64,
|
||||
installed_version: String,
|
||||
latest_version: String,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated)
|
||||
> Duration::from_secs((self.data_refresh_interval * 60) * 60)
|
||||
{
|
||||
let client = reqwest::blocking::Client::new();
|
||||
if let Ok(response) = client
|
||||
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
|
||||
.header("User-Agent", "komorebi-bar-version-checker")
|
||||
.send()
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
if let Ok(release) =
|
||||
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
|
||||
{
|
||||
let trimmed = release.tag_name.trim_start_matches("v");
|
||||
self.latest_version = trimmed.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
if self.latest_version > self.installed_version {
|
||||
format!("Update available! v{}", self.latest_version)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Update {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ROCKET_LAUNCH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("explorer.exe")
|
||||
.args([format!(
|
||||
"https://github.com/LGUG2Z/komorebi/releases/v{}",
|
||||
self.latest_version
|
||||
)])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
komorebi-bar/src/widgets/widget.rs
Normal file
89
komorebi-bar/src/widgets/widget.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::battery::Battery;
|
||||
use crate::widgets::battery::BatteryConfig;
|
||||
use crate::widgets::cpu::Cpu;
|
||||
use crate::widgets::cpu::CpuConfig;
|
||||
use crate::widgets::date::Date;
|
||||
use crate::widgets::date::DateConfig;
|
||||
use crate::widgets::keyboard::Keyboard;
|
||||
use crate::widgets::keyboard::KeyboardConfig;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::KomorebiConfig;
|
||||
use crate::widgets::media::Media;
|
||||
use crate::widgets::media::MediaConfig;
|
||||
use crate::widgets::memory::Memory;
|
||||
use crate::widgets::memory::MemoryConfig;
|
||||
use crate::widgets::network::Network;
|
||||
use crate::widgets::network::NetworkConfig;
|
||||
use crate::widgets::storage::Storage;
|
||||
use crate::widgets::storage::StorageConfig;
|
||||
use crate::widgets::time::Time;
|
||||
use crate::widgets::time::TimeConfig;
|
||||
use crate::widgets::update::Update;
|
||||
use crate::widgets::update::UpdateConfig;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Ui;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub trait BarWidget {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum WidgetConfig {
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
Keyboard(KeyboardConfig),
|
||||
Komorebi(KomorebiConfig),
|
||||
Media(MediaConfig),
|
||||
Memory(MemoryConfig),
|
||||
Network(NetworkConfig),
|
||||
Storage(StorageConfig),
|
||||
Time(TimeConfig),
|
||||
Update(UpdateConfig),
|
||||
}
|
||||
|
||||
impl WidgetConfig {
|
||||
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
|
||||
match self {
|
||||
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
||||
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
||||
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
||||
WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
|
||||
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
|
||||
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
||||
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
|
||||
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
|
||||
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
match self {
|
||||
WidgetConfig::Battery(config) => config.enable,
|
||||
WidgetConfig::Cpu(config) => config.enable,
|
||||
WidgetConfig::Date(config) => config.enable,
|
||||
WidgetConfig::Keyboard(config) => config.enable,
|
||||
WidgetConfig::Komorebi(config) => {
|
||||
config.workspaces.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.layout.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.focused_container.as_ref().is_some_and(|w| w.enable)
|
||||
|| config
|
||||
.configuration_switcher
|
||||
.as_ref()
|
||||
.is_some_and(|w| w.enable)
|
||||
}
|
||||
WidgetConfig::Media(config) => config.enable,
|
||||
WidgetConfig::Memory(config) => config.enable,
|
||||
WidgetConfig::Network(config) => config.enable,
|
||||
WidgetConfig::Storage(config) => config.enable,
|
||||
WidgetConfig::Time(config) => config.enable,
|
||||
WidgetConfig::Update(config) => config.enable,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.31"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -10,3 +10,7 @@ komorebi = { path = "../komorebi" }
|
||||
|
||||
uds_windows = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["komorebi/schemars"]
|
||||
|
||||
@@ -2,29 +2,39 @@
|
||||
#![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::border_manager::BorderInfo;
|
||||
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;
|
||||
pub use komorebi::core::replace_env_in_path;
|
||||
pub use komorebi::core::AnimationStyle;
|
||||
pub use komorebi::core::ApplicationIdentifier;
|
||||
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;
|
||||
pub use komorebi::core::Direction;
|
||||
pub use komorebi::core::FloatingLayerBehaviour;
|
||||
pub use komorebi::core::FocusFollowsMouseImplementation;
|
||||
pub use komorebi::core::HidingBehaviour;
|
||||
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,36 @@ 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::win32_display_data;
|
||||
pub use komorebi::window::Window;
|
||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::workspace::WorkspaceGlobals;
|
||||
pub use komorebi::workspace::WorkspaceLayer;
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::AppSpecificConfigurationPath;
|
||||
pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::Colour;
|
||||
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::Rgb;
|
||||
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;
|
||||
|
||||
@@ -68,6 +93,20 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
s
|
||||
});
|
||||
stream.write_all(msgs.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.31"
|
||||
version = "0.1.36"
|
||||
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.5", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
@@ -41,7 +41,9 @@ struct BorderColours {
|
||||
single: Color32,
|
||||
stack: Color32,
|
||||
monocle: Color32,
|
||||
floating: Color32,
|
||||
unfocused: Color32,
|
||||
unfocused_locked: Color32,
|
||||
}
|
||||
|
||||
struct BorderConfig {
|
||||
@@ -101,7 +103,7 @@ impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||
let name = value
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
|
||||
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
|
||||
|
||||
Self {
|
||||
layout,
|
||||
@@ -154,7 +156,9 @@ impl KomorebiGui {
|
||||
single: colour32(global_state.border_colours.single),
|
||||
stack: colour32(global_state.border_colours.stack),
|
||||
monocle: colour32(global_state.border_colours.monocle),
|
||||
floating: colour32(global_state.border_colours.floating),
|
||||
unfocused: colour32(global_state.border_colours.unfocused),
|
||||
unfocused_locked: colour32(global_state.border_colours.unfocused_locked),
|
||||
};
|
||||
|
||||
let border_config = BorderConfig {
|
||||
@@ -215,7 +219,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);
|
||||
|
||||
@@ -377,6 +381,22 @@ impl eframe::App for KomorebiGui {
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Floating", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.floating,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::Floating,
|
||||
self.border_config.border_colours.floating.r() as u32,
|
||||
self.border_config.border_colours.floating.g() as u32,
|
||||
self.border_config.border_colours.floating.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Unfocused", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
@@ -391,6 +411,22 @@ impl eframe::App for KomorebiGui {
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Unfocused Locked", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.unfocused_locked,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::UnfocusedLocked,
|
||||
self.border_config.border_colours.unfocused_locked.r() as u32,
|
||||
self.border_config.border_colours.unfocused_locked.g() as u32,
|
||||
self.border_config.border_colours.unfocused_locked.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.31"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "c11fbe2a3a4681485c5065b899a4c4d85fad3b04" }
|
||||
#catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f579847bf2f552b144361d5a78ed8cf360b55cbb" }
|
||||
catppuccin-egui = { version = "5", default-features = false, features = ["egui29"] }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_variant = "0.1"
|
||||
strum = "0.26"
|
||||
strum = { workspace = true }
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars"]
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
use hex_color::HexColor;
|
||||
use komorebi_themes::Color32;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::gen::SchemaGenerator;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::InstanceType;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::Schema;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::SchemaObject;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use crate::Color32;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum Colour {
|
||||
/// Colour represented as RGB
|
||||
@@ -51,10 +56,11 @@ impl From<Colour> for Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Hex(pub HexColor);
|
||||
|
||||
impl JsonSchema for Hex {
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for Hex {
|
||||
fn schema_name() -> String {
|
||||
String::from("Hex")
|
||||
}
|
||||
@@ -78,7 +84,8 @@ impl From<Colour> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
@@ -120,8 +127,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
77
komorebi-themes/src/generator.rs
Normal file
77
komorebi-themes/src/generator.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::colour::Colour;
|
||||
use crate::colour::Hex;
|
||||
use crate::Base16ColourPalette;
|
||||
use hex_color::HexColor;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum ThemeVariant {
|
||||
#[default]
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
|
||||
impl Display for ThemeVariant {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ThemeVariant::Dark => write!(f, "dark"),
|
||||
ThemeVariant::Light => write!(f, "light"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThemeVariant> for flavours::operations::generate::Mode {
|
||||
fn from(value: ThemeVariant) -> Self {
|
||||
match value {
|
||||
ThemeVariant::Dark => Self::Dark,
|
||||
ThemeVariant::Light => Self::Light,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_base16_palette(
|
||||
image_path: &Path,
|
||||
variant: ThemeVariant,
|
||||
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
|
||||
Base16ColourPalette::try_from(
|
||||
&flavours::operations::generate::generate(image_path, variant.into(), false)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
|
||||
type Error = hex_color::ParseHexColorError;
|
||||
|
||||
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
|
||||
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
|
||||
if fixed.len() != 16 {
|
||||
return Err(hex_color::ParseHexColorError::Empty);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
|
||||
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
|
||||
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
|
||||
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
|
||||
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
|
||||
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
|
||||
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
|
||||
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
|
||||
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
|
||||
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
|
||||
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
|
||||
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
|
||||
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
|
||||
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
|
||||
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
|
||||
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,32 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub mod colour;
|
||||
mod generator;
|
||||
|
||||
pub use generator::generate_base16_palette;
|
||||
pub use generator::ThemeVariant;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::colour::Colour;
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
use eframe::egui::style::Selection;
|
||||
use eframe::egui::style::WidgetVisuals;
|
||||
use eframe::egui::style::Widgets;
|
||||
pub use eframe::egui::Color32;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::Visuals;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
@@ -24,6 +39,140 @@ pub enum Theme {
|
||||
name: Base16,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
/// A custom base16 palette
|
||||
Custom {
|
||||
palette: Box<Base16ColourPalette>,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Base16ColourPalette {
|
||||
pub base_00: Colour,
|
||||
pub base_01: Colour,
|
||||
pub base_02: Colour,
|
||||
pub base_03: Colour,
|
||||
pub base_04: Colour,
|
||||
pub base_05: Colour,
|
||||
pub base_06: Colour,
|
||||
pub base_07: Colour,
|
||||
pub base_08: Colour,
|
||||
pub base_09: Colour,
|
||||
pub base_0a: Colour,
|
||||
pub base_0b: Colour,
|
||||
pub base_0c: Colour,
|
||||
pub base_0d: Colour,
|
||||
pub base_0e: Colour,
|
||||
pub base_0f: Colour,
|
||||
}
|
||||
|
||||
impl Base16ColourPalette {
|
||||
pub fn background(self) -> Color32 {
|
||||
self.base_01.into()
|
||||
}
|
||||
pub fn style(self) -> Style {
|
||||
let original = Style::default();
|
||||
Style {
|
||||
visuals: Visuals {
|
||||
widgets: Widgets {
|
||||
noninteractive: WidgetVisuals {
|
||||
bg_fill: self.base_01.into(),
|
||||
weak_bg_fill: self.base_01.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_02.into(),
|
||||
..original.visuals.widgets.noninteractive.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_05.into(),
|
||||
..original.visuals.widgets.noninteractive.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.noninteractive
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
bg_fill: self.base_02.into(),
|
||||
weak_bg_fill: self.base_02.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
|
||||
..original.visuals.widgets.inactive.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_05.into(),
|
||||
..original.visuals.widgets.inactive.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.inactive
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
bg_fill: self.base_02.into(),
|
||||
weak_bg_fill: self.base_02.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_03.into(),
|
||||
..original.visuals.widgets.hovered.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.widgets.hovered.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.hovered
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
bg_fill: self.base_02.into(),
|
||||
weak_bg_fill: self.base_02.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_03.into(),
|
||||
..original.visuals.widgets.hovered.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.widgets.hovered.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.active
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
bg_fill: self.base_01.into(),
|
||||
weak_bg_fill: self.base_01.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_02.into(),
|
||||
..original.visuals.widgets.open.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.widgets.open.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.open
|
||||
},
|
||||
},
|
||||
selection: Selection {
|
||||
bg_fill: self.base_02.into(),
|
||||
stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.selection.stroke
|
||||
},
|
||||
},
|
||||
hyperlink_color: self.base_08.into(),
|
||||
faint_bg_color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
|
||||
extreme_bg_color: self.base_00.into(),
|
||||
code_bg_color: self.base_02.into(),
|
||||
warn_fg_color: self.base_0c.into(),
|
||||
error_fg_color: self.base_0b.into(),
|
||||
window_shadow: Shadow {
|
||||
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
|
||||
..original.visuals.window_shadow
|
||||
},
|
||||
window_fill: self.base_01.into(),
|
||||
window_stroke: Stroke {
|
||||
color: self.base_02.into(),
|
||||
..original.visuals.window_stroke
|
||||
},
|
||||
panel_fill: self.base_01.into(),
|
||||
popup_shadow: Shadow {
|
||||
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
|
||||
..original.visuals.popup_shadow
|
||||
},
|
||||
..original.visuals
|
||||
},
|
||||
..original
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
@@ -44,11 +193,12 @@ impl Theme {
|
||||
.to_string()
|
||||
})
|
||||
.collect(),
|
||||
Theme::Custom { .. } => vec!["Custom".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
pub enum Base16Value {
|
||||
Base00,
|
||||
Base01,
|
||||
@@ -69,30 +219,55 @@ pub enum Base16Value {
|
||||
Base0F,
|
||||
}
|
||||
|
||||
pub enum Base16Wrapper {
|
||||
Base16(Base16),
|
||||
Custom(Box<Base16ColourPalette>),
|
||||
}
|
||||
|
||||
impl Base16Value {
|
||||
pub fn color32(&self, theme: Base16) -> Color32 {
|
||||
match self {
|
||||
Base16Value::Base00 => theme.base00(),
|
||||
Base16Value::Base01 => theme.base01(),
|
||||
Base16Value::Base02 => theme.base02(),
|
||||
Base16Value::Base03 => theme.base03(),
|
||||
Base16Value::Base04 => theme.base04(),
|
||||
Base16Value::Base05 => theme.base05(),
|
||||
Base16Value::Base06 => theme.base06(),
|
||||
Base16Value::Base07 => theme.base07(),
|
||||
Base16Value::Base08 => theme.base08(),
|
||||
Base16Value::Base09 => theme.base09(),
|
||||
Base16Value::Base0A => theme.base0a(),
|
||||
Base16Value::Base0B => theme.base0b(),
|
||||
Base16Value::Base0C => theme.base0c(),
|
||||
Base16Value::Base0D => theme.base0d(),
|
||||
Base16Value::Base0E => theme.base0e(),
|
||||
Base16Value::Base0F => theme.base0f(),
|
||||
pub fn color32(&self, theme: Base16Wrapper) -> Color32 {
|
||||
match theme {
|
||||
Base16Wrapper::Base16(theme) => match self {
|
||||
Base16Value::Base00 => theme.base00(),
|
||||
Base16Value::Base01 => theme.base01(),
|
||||
Base16Value::Base02 => theme.base02(),
|
||||
Base16Value::Base03 => theme.base03(),
|
||||
Base16Value::Base04 => theme.base04(),
|
||||
Base16Value::Base05 => theme.base05(),
|
||||
Base16Value::Base06 => theme.base06(),
|
||||
Base16Value::Base07 => theme.base07(),
|
||||
Base16Value::Base08 => theme.base08(),
|
||||
Base16Value::Base09 => theme.base09(),
|
||||
Base16Value::Base0A => theme.base0a(),
|
||||
Base16Value::Base0B => theme.base0b(),
|
||||
Base16Value::Base0C => theme.base0c(),
|
||||
Base16Value::Base0D => theme.base0d(),
|
||||
Base16Value::Base0E => theme.base0e(),
|
||||
Base16Value::Base0F => theme.base0f(),
|
||||
},
|
||||
Base16Wrapper::Custom(colours) => match self {
|
||||
Base16Value::Base00 => colours.base_00.into(),
|
||||
Base16Value::Base01 => colours.base_01.into(),
|
||||
Base16Value::Base02 => colours.base_02.into(),
|
||||
Base16Value::Base03 => colours.base_03.into(),
|
||||
Base16Value::Base04 => colours.base_04.into(),
|
||||
Base16Value::Base05 => colours.base_05.into(),
|
||||
Base16Value::Base06 => colours.base_06.into(),
|
||||
Base16Value::Base07 => colours.base_07.into(),
|
||||
Base16Value::Base08 => colours.base_08.into(),
|
||||
Base16Value::Base09 => colours.base_09.into(),
|
||||
Base16Value::Base0A => colours.base_0a.into(),
|
||||
Base16Value::Base0B => colours.base_0b.into(),
|
||||
Base16Value::Base0C => colours.base_0c.into(),
|
||||
Base16Value::Base0D => colours.base_0d.into(),
|
||||
Base16Value::Base0E => colours.base_0e.into(),
|
||||
Base16Value::Base0F => colours.base_0f.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
pub enum Catppuccin {
|
||||
Frappe,
|
||||
Latte,
|
||||
@@ -117,7 +292,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,8 +1,7 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.31"
|
||||
version = "0.1.36"
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
|
||||
@@ -20,22 +19,22 @@ ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
getset = "0.1"
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
hotwatch = { workspace = true }
|
||||
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 }
|
||||
powershell_script = "1.0"
|
||||
regex = "1"
|
||||
schemars = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
strum = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
@@ -45,13 +44,21 @@ 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.52"
|
||||
winreg = "0.55"
|
||||
serde_with = { version = "3.12", features = ["schemars_0_8"] }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||
schemars = ["dep:schemars"]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use shadow_rs::ShadowBuilder;
|
||||
|
||||
fn main() {
|
||||
shadow_rs::new().unwrap();
|
||||
ShadowBuilder::builder().build().unwrap();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -13,7 +11,8 @@ use super::ANIMATION_DURATION_GLOBAL;
|
||||
use super::ANIMATION_FPS;
|
||||
use super::ANIMATION_MANAGER;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct AnimationEngine;
|
||||
|
||||
impl AnimationEngine {
|
||||
|
||||
@@ -18,11 +18,12 @@ pub mod prefix;
|
||||
pub mod render_dispatcher;
|
||||
pub use render_dispatcher::RenderDispatcher;
|
||||
pub mod style;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
Prefix(HashMap<AnimationPrefix, T>),
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnimationPrefix {
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::RENDER_TARGETS;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::LazyLock;
|
||||
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;
|
||||
@@ -30,6 +27,7 @@ use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||
@@ -43,30 +41,54 @@ use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
||||
use windows::Win32::Graphics::Gdi::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SIZE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
||||
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"),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -89,14 +111,40 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
true.into()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Border {
|
||||
pub hwnd: isize,
|
||||
pub id: String,
|
||||
pub monitor_idx: Option<usize>,
|
||||
pub render_target: Option<RenderTarget>,
|
||||
pub tracking_hwnd: isize,
|
||||
pub window_rect: Rect,
|
||||
pub window_kind: WindowKind,
|
||||
pub style: BorderStyle,
|
||||
pub width: i32,
|
||||
pub offset: i32,
|
||||
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
||||
pub rounded_rect: D2D1_ROUNDED_RECT,
|
||||
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
||||
}
|
||||
|
||||
impl From<isize> for Border {
|
||||
fn from(value: isize) -> Self {
|
||||
Self { hwnd: value }
|
||||
Self {
|
||||
hwnd: value,
|
||||
id: String::new(),
|
||||
monitor_idx: None,
|
||||
render_target: None,
|
||||
tracking_hwnd: 0,
|
||||
window_rect: Rect::default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
||||
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
||||
brushes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +153,11 @@ impl Border {
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
pub fn create(
|
||||
id: &str,
|
||||
tracking_hwnd: isize,
|
||||
monitor_idx: usize,
|
||||
) -> color_eyre::Result<Box<Self>> {
|
||||
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
@@ -121,18 +173,42 @@ impl Border {
|
||||
|
||||
let _ = WindowsApi::register_class_w(&window_class);
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
let (border_sender, border_receiver) = mpsc::channel();
|
||||
|
||||
let instance = h_module.0 as isize;
|
||||
let container_id = id.to_owned();
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
let mut border = Self {
|
||||
hwnd: 0,
|
||||
id: container_id,
|
||||
monitor_idx: Some(monitor_idx),
|
||||
render_target: None,
|
||||
tracking_hwnd,
|
||||
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: Default::default(),
|
||||
rounded_rect: Default::default(),
|
||||
brushes: HashMap::new(),
|
||||
};
|
||||
|
||||
let border_pointer = &raw mut border;
|
||||
let hwnd =
|
||||
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
|
||||
|
||||
let boxed = unsafe {
|
||||
(*border_pointer).hwnd = hwnd;
|
||||
Box::from_raw(border_pointer)
|
||||
};
|
||||
border_sender.send(boxed)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -145,8 +221,7 @@ impl Border {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let hwnd = hwnd_receiver.recv()?;
|
||||
let border = Self { hwnd };
|
||||
let mut border = border_receiver.recv()?;
|
||||
|
||||
// I have literally no idea, apparently this is to get rid of the black pixels
|
||||
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
|
||||
@@ -166,8 +241,14 @@ impl Border {
|
||||
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
|
||||
}
|
||||
|
||||
border.update_brushes()?;
|
||||
|
||||
Ok(border)
|
||||
}
|
||||
|
||||
pub fn update_brushes(&mut self) -> color_eyre::Result<()> {
|
||||
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
|
||||
hwnd: HWND(windows_api::as_ptr!(hwnd)),
|
||||
hwnd: HWND(windows_api::as_ptr!(self.hwnd)),
|
||||
pixelSize: Default::default(),
|
||||
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
|
||||
};
|
||||
@@ -188,44 +269,66 @@ impl Border {
|
||||
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
|
||||
} {
|
||||
Ok(render_target) => unsafe {
|
||||
self.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
for window_kind in [
|
||||
WindowKind::Single,
|
||||
WindowKind::Stack,
|
||||
WindowKind::Monocle,
|
||||
WindowKind::Unfocused,
|
||||
WindowKind::Floating,
|
||||
WindowKind::UnfocusedLocked,
|
||||
] {
|
||||
let color = window_kind_colour(window_kind);
|
||||
let color = D2D1_COLOR_F {
|
||||
r: ((color & 0xFF) as f32) / 255.0,
|
||||
g: (((color >> 8) & 0xFF) as f32) / 255.0,
|
||||
b: (((color >> 16) & 0xFF) as f32) / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
if let Ok(brush) =
|
||||
render_target.CreateSolidColorBrush(&color, Some(&self.brush_properties))
|
||||
{
|
||||
self.brushes.insert(window_kind, brush);
|
||||
}
|
||||
}
|
||||
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.insert(hwnd, render_target);
|
||||
Ok(border)
|
||||
|
||||
self.render_target = Some(RenderTarget(render_target));
|
||||
|
||||
self.rounded_rect = {
|
||||
let radius = 8.0 + self.width as f32 / 2.0;
|
||||
D2D1_ROUNDED_RECT {
|
||||
rect: Default::default(),
|
||||
radiusX: radius,
|
||||
radiusY: radius,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.remove(&self.hwnd);
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn update(&self, rect: &Rect, should_invalidate: bool) -> color_eyre::Result<()> {
|
||||
// Make adjustments to the border
|
||||
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
||||
let mut rect = *rect;
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::Relaxed));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::Relaxed));
|
||||
rect.add_margin(self.width);
|
||||
rect.add_padding(-self.offset);
|
||||
|
||||
// Update the position of the border if required
|
||||
// This effectively handles WM_MOVE
|
||||
// Also if I remove this no borders render at all lol
|
||||
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
|
||||
}
|
||||
|
||||
// Invalidate the rect to trigger the callback to update colours etc.
|
||||
if should_invalidate {
|
||||
self.invalidate();
|
||||
}
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -236,50 +339,83 @@ impl Border {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_SIZE | WM_PAINT => {
|
||||
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
|
||||
let render_targets = RENDER_TARGETS.lock();
|
||||
if let Some(render_target) = render_targets.get(&(window.0 as isize)) {
|
||||
let pixel_size = D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
};
|
||||
WM_SETCURSOR => match LoadCursorW(None, IDC_ARROW) {
|
||||
Ok(cursor) => {
|
||||
SetCursor(Some(cursor));
|
||||
LRESULT(0)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
LRESULT(1)
|
||||
}
|
||||
},
|
||||
WM_CREATE => {
|
||||
let mut border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
if border_pointer.is_null() {
|
||||
let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _;
|
||||
border_pointer = (*create_struct).lpCreateParams as *mut _;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _);
|
||||
}
|
||||
|
||||
let rect = D2D_RECT_F {
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_DESTROY => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
// we don't actually want to destroy the window here, just hide it for quicker
|
||||
// visual feedback to the user; the actual destruction will be handled by the
|
||||
// core border manager loop
|
||||
WindowsApi::hide_window(window.0 as isize);
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_LOCATIONCHANGE => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
|
||||
let old_rect = (*border_pointer).window_rect;
|
||||
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
|
||||
|
||||
(*border_pointer).window_rect = rect;
|
||||
|
||||
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&pixel_size);
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
// Get window kind and color
|
||||
let window_kind = FOCUS_STATE
|
||||
.lock()
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused);
|
||||
|
||||
let color = window_kind_colour(window_kind);
|
||||
let color = D2D1_COLOR_F {
|
||||
r: ((color & 0xFF) as f32) / 255.0,
|
||||
g: (((color >> 8) & 0xFF) as f32) / 255.0,
|
||||
b: (((color >> 16) & 0xFF) as f32) / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
if let Ok(brush) = render_target
|
||||
.CreateSolidColorBrush(&color, Some(BRUSH_PROPERTIES.deref()))
|
||||
{
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match STYLE.load() {
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
@@ -293,31 +429,17 @@ impl Border {
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
let radius = 8.0 + border_width as f32 / 2.0;
|
||||
let rounded_rect = D2D1_ROUNDED_RECT {
|
||||
rect,
|
||||
radiusX: radius,
|
||||
radiusY: radius,
|
||||
};
|
||||
|
||||
render_target.DrawRoundedRectangle(
|
||||
&rounded_rect,
|
||||
&brush,
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
let rect = D2D_RECT_F {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
};
|
||||
|
||||
render_target.DrawRectangle(
|
||||
&rect,
|
||||
&brush,
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
@@ -326,17 +448,102 @@ impl Border {
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
|
||||
// If we don't do this we'll get spammed with WM_PAINT according to Raymond Chen
|
||||
// https://stackoverflow.com/questions/41783234/why-does-my-call-to-d2d1rendertargetdrawtext-result-in-a-wm-paint-being-se#comment70756781_41783234
|
||||
let _ = BeginPaint(window, &mut PAINTSTRUCT::default());
|
||||
let _ = EndPaint(window, &PAINTSTRUCT::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_PAINT => {
|
||||
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
|
||||
let border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
|
||||
// Update position to update the ZOrder
|
||||
let border_window_rect = (*border_pointer).window_rect;
|
||||
|
||||
tracing::trace!("updating border position");
|
||||
if let Err(error) =
|
||||
(*border_pointer).set_position(&border_window_rect, reference_hwnd)
|
||||
{
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
// Get window kind and color
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
(*border_pointer).style = STYLE.load();
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = ValidateRect(Option::from(window), None);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ use std::collections::VecDeque;
|
||||
|
||||
use getset::Getters;
|
||||
use nanoid::nanoid;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
@@ -52,13 +52,17 @@ impl Container {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the unfocused windows of the container and restores the focused one. This function
|
||||
/// is used to make sure we update the window that should be shown on a stack. If the container
|
||||
/// isn't a stack this function won't change anything.
|
||||
pub fn load_focused_window(&mut self) {
|
||||
let focused_idx = self.focused_window_idx();
|
||||
|
||||
for (i, window) in self.windows_mut().iter_mut().enumerate() {
|
||||
if i == focused_idx {
|
||||
window.restore();
|
||||
window.restore_with_border(false);
|
||||
} else {
|
||||
window.hide();
|
||||
window.hide_with_border(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +79,18 @@ impl Container {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
|
||||
for (idx, window) in self.windows().iter().enumerate() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||
for window in self.windows() {
|
||||
if window.hwnd == hwnd {
|
||||
@@ -86,14 +102,13 @@ impl Container {
|
||||
}
|
||||
|
||||
pub fn idx_for_window(&self, hwnd: isize) -> Option<usize> {
|
||||
let mut idx = None;
|
||||
for (i, window) in self.windows().iter().enumerate() {
|
||||
if window.hwnd == hwnd {
|
||||
idx = Option::from(i);
|
||||
return Option::from(i);
|
||||
}
|
||||
}
|
||||
|
||||
idx
|
||||
None
|
||||
}
|
||||
|
||||
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
|
||||
@@ -125,3 +140,114 @@ impl Container {
|
||||
self.windows.focus(idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_contains_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should return true for existing windows
|
||||
assert!(container.contains_window(1));
|
||||
assert_eq!(container.idx_for_window(1), Some(1));
|
||||
|
||||
// Should return false since window 4 doesn't exist
|
||||
assert!(!container.contains_window(4));
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_window_by_idx() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Remove window 1
|
||||
container.remove_window_by_idx(1);
|
||||
|
||||
// Should only have 2 windows left
|
||||
assert_eq!(container.windows().len(), 2);
|
||||
|
||||
// Should return false since window 1 was removed
|
||||
assert!(!container.contains_window(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_focused_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should be focused on the last created window
|
||||
assert_eq!(container.focused_window_idx(), 2);
|
||||
|
||||
// Remove the focused window
|
||||
container.remove_focused_window();
|
||||
|
||||
// Should be focused on the window before the removed one
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
|
||||
// Should only have 2 windows left
|
||||
assert_eq!(container.windows().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
container.add_window(Window::from(1));
|
||||
|
||||
assert_eq!(container.windows().len(), 1);
|
||||
assert_eq!(container.focused_window_idx(), 0);
|
||||
assert!(container.contains_window(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should focus on the last created window
|
||||
assert_eq!(container.focused_window_idx(), 2);
|
||||
|
||||
// focus on the window at index 1
|
||||
container.focus_window(1);
|
||||
|
||||
// Should be focused on window 1
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
|
||||
// focus on the window at index 0
|
||||
container.focus_window(0);
|
||||
|
||||
// Should be focused on window 0
|
||||
assert_eq!(container.focused_window_idx(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idx_for_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should return the index of the window
|
||||
assert_eq!(container.idx_for_window(1), Some(1));
|
||||
|
||||
// Should return None since window 4 doesn't exist
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum AnimationStyle {
|
||||
Linear,
|
||||
EaseInSine,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
@@ -603,18 +602,8 @@ impl Arrangement for CustomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user