Compare commits

..

7 Commits

Author SHA1 Message Date
Ivan Molodetskikh 9cb2fe4eeb wip extra overview scale 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh cc2549323d wip 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 43c1592ab7 layout/tab_indicator: Use round_max1 where appropriate 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 78fe4a68db layout/monitor: Extract workspaces_render_geo() 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 4fe2722a28 Simplify condition 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 7da5fc6169 Extract is_layout_obscured_under() 2025-04-25 11:23:06 +03:00
Ivan Molodetskikh 3d4b762bcc Put the top layer above bottom and background layer popups
Makes it consistent with how window popups are below the top layer, also
will make more sense for the overview.
2025-04-25 11:23:06 +03:00
1709 changed files with 15759 additions and 49281 deletions
-11
View File
@@ -1,12 +1 @@
# LFS configuration for images from the wiki
*.png filter=lfs diff=lfs merge=lfs -text
# Exclude LFS-tracked files from the tarball
/docs/wiki/img/ export-ignore
# exclude .gitattributes itself from the tarball
.gitattributes export-ignore
# tip: can be tested using
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
-7
View File
@@ -10,13 +10,6 @@ assignees: ''
<!-- Please describe the issue here at the top, then fill in the system information below. -->
<!-- Attaching your full niri config can help diagnose the problem. -->
<details><summary>Config</summary>
```kdl
insert config here
```
</details>
<!--
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
+2 -2
View File
@@ -1,9 +1,9 @@
contact_links:
- name: Feature request
url: https://github.com/niri-wm/niri/discussions/new?category=ideas
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
- name: Ask a question
url: https://github.com/niri-wm/niri/discussions/new?category=q-a
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
about: Question about niri (start a Discussion)
- name: Matrix room
url: https://matrix.to/#/#niri:matrix.org
+4 -6
View File
@@ -3,7 +3,7 @@ updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
interval: "daily"
groups:
smithay:
patterns:
@@ -13,12 +13,10 @@ updates:
update-types:
- "minor"
- "patch"
cooldown:
default-days: 7
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
interval: "daily"
ignore:
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
+57 -139
View File
@@ -8,48 +8,27 @@ on:
- cron: '0 0 1 * *' # Monthly
env:
RUN_SLOW_TESTS: 1
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
DEPS_APK: cargo clang-libclang eudev-dev glib-dev libdisplay-info-dev libinput-dev libseat-dev libxkbcommon-dev mesa-dev pango-dev pipewire-dev tar
DEPS_PKG: git curl rust llvm pkgconf pixman libudev-devd libdisplay-info seatd libinput libxkbcommon pipewire mesa-libs cairo devel/glib20 gettext-runtime harfbuzz pango
jobs:
test:
strategy:
fail-fast: false
name: test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
show-progress: false
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build tests
run: cargo test --no-run --all --exclude niri-visual-tests
- name: Test
run: cargo test --all --exclude niri-visual-tests -- --nocapture
build:
strategy:
fail-fast: false
name: check feature combinations
matrix:
configuration: [debug, release]
include:
- configuration: release
release-flag: '--release'
name: test - ${{ matrix.configuration }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
@@ -61,67 +40,53 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.configuration }}
- name: Check (no default features)
run: cargo check --no-default-features
run: cargo check ${{ matrix.release-flag }} --no-default-features
- name: Check (just dbus)
run: cargo check --no-default-features --features dbus
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dbus
- name: Check (just systemd)
run: cargo check --no-default-features --features systemd
run: cargo check ${{ matrix.release-flag }} --no-default-features --features systemd
- name: Check (just dinit)
run: cargo check --no-default-features --features dinit
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dinit
- name: Check (just xdp-gnome-screencast)
run: cargo check --no-default-features --features xdp-gnome-screencast
run: cargo check ${{ matrix.release-flag }} --no-default-features --features xdp-gnome-screencast
- name: Check
run: cargo check
run: cargo check ${{ matrix.release-flag }}
- name: Build (with profiling)
run: cargo build --features profile-with-tracy
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
build-musl:
strategy:
fail-fast: false
- name: Build tests
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
name: alpine musl
runs-on: ubuntu-24.04
container: alpine:3
steps:
- uses: actions/checkout@v6
with:
show-progress: false
- name: Install Deps
run: apk add --no-cache ${{ env.DEPS_APK }}
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --no-default-features --features dbus,xdp-gnome-screencast
- name: Test
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
# Job that runs randomized tests for a longer period of time.
# Also runs normal slow tests.
randomized-tests:
strategy:
fail-fast: false
name: randomized and slow tests
name: randomized tests
runs-on: ubuntu-24.04
env:
RUST_BACKTRACE: 1
RUN_SLOW_TESTS: 1
PROPTEST_CASES: 200000
PROPTEST_MAX_LOCAL_REJECTS: 200000
PROPTEST_MAX_GLOBAL_REJECTS: 200000
PROPTEST_MAX_SHRINK_ITERS: 200000
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
@@ -148,7 +113,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
@@ -172,7 +137,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
@@ -181,7 +146,7 @@ jobs:
sudo apt-get update -y
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
- uses: dtolnay/rust-toolchain@1.85.0
- uses: dtolnay/rust-toolchain@1.80.1
- uses: Swatinem/rust-cache@v2
@@ -195,7 +160,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
@@ -217,7 +182,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
@@ -230,10 +195,10 @@ jobs:
fedora:
runs-on: ubuntu-24.04
container: fedora:42
container: fedora:41
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
@@ -245,117 +210,70 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo build --all
freebsd:
runs-on: ubuntu-24.04
env:
CARGO_HOME: /home/runner/work/niri/niri/cargo-home
steps:
- uses: actions/checkout@v6
with:
show-progress: false
# Remove man-db triggers to speed up Ubuntu upgrade by a minute or two during vmactions/freebsd-vm action run.
- run: |
sudo rm /var/lib/dpkg/info/man-db.*
- name: Build
uses: vmactions/freebsd-vm@v1
with:
release: "15.0"
copyback: false
prepare: |
pkg update -f
pkg install -y ${{ env.DEPS_PKG }}
run: |
curl -o patch-pipewire_init 'https://cgit.freebsd.org/ports/plain/x11-wm/niri/files/patch-pipewire_init?id=cadf6784d264cf780b6e0ad59bd15b831d36cf80'
export CARGO_HOME="$PWD/cargo-home"
cargo fetch
( cd $CARGO_HOME/registry/src/index.crates.io-*/; patch -p1 < $CARGO_HOME/../patch-pipewire_init; )
cargo build \
--offline \
--no-default-features --features dbus,xdp-gnome-screencast
nix:
runs-on: ubuntu-24.04
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@v1.3.1
with:
dotnet: false
large-packages: false
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check flake inputs
uses: DeterminateSystems/flake-checker-action@v4
continue-on-error: true
- name: Install Nix
uses: cachix/install-nix-action@v31
uses: DeterminateSystems/nix-installer-action@v3
continue-on-error: true
- run: nix flake check
continue-on-error: true
check-links:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
with:
args: --offline --include-fragments 'wiki/*.md'
publish-wiki:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- publish-docs
- build
- check-links
permissions:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
lfs: true
show-progress: false
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
with:
path: docs/wiki/
publish-docs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test
rustdoc:
needs: build
permissions:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
lfs: true
show-progress: false
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Install the project
run: uv sync --locked --all-extras --dev
working-directory: docs/
- name: Generate niri documentation
run: uv run mkdocs build
working-directory: docs/
- uses: dtolnay/rust-toolchain@stable
- name: Generate rustdoc documentation
- name: Generate documentation
run: cargo doc --no-deps -p niri-ipc
- run: mkdir -p publish/niri_ipc
- run: cp -r ./target/doc/* ./publish/
- run: cp -r ./docs/site/* ./publish/
- run: cp ./resources/rustdoc-index.html ./target/doc/index.html
- name: Deploy documentation
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./publish
publish_dir: ./target/doc
force_orphan: true
+4 -7
View File
@@ -22,18 +22,15 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check for unreplaced "Since:" in the wiki
run: |
# Fail if a match is found (exit code 0)
grep --recursive 'Since: next release' docs/wiki && exit 1
# Fail if grep failed (exit code 2)
status=$?
if [ $status -ne 1 ]; then exit $status; fi
if grep --recursive 'Since: next release' wiki; then
exit 1
fi
- name: Install dependencies
run: |
-2
View File
@@ -1,4 +1,2 @@
/target
/result
.idea
-114
View File
@@ -1,114 +0,0 @@
# Contributing to niri
Thanks for your interest in niri!
The project has grown quite a bit, and we could use all help that we can.
Make sure to join our Matrix chat if you have any questions or want to discuss anything: https://matrix.to/#/#niri:matrix.org
## Issues and discussions
This is a good way to help many new and existing users without programming knowledge.
- Answer and help people in GitHub issues and discussions.
- Check and point out duplicate issues.
- Check for issues that are likely application bugs (and not niri bugs).
- Ask or try to reproduce on another non-Smithay-based compositor (sway, KDE/KWin, GNOME/Mutter). If the issue reproduces, it's likely an application bug.
- Ask or try to reproduce on another *Smithay-based* compositor ([cosmic-comp], [anvil]). If the issue reproduces only on Smithay compositors, it may be a Smithay bug.
- Make sure you're testing the Wayland version of the app on all compositors. Apps may silently use X11 when an X11 `$DISPLAY` is available.
- Problems with X11 apps should be reported to [xwayland-satellite]. When testing xwayland-satellite on different compositors, make sure you use xwayland-satellite's `$DISPLAY` (rather than another compositor's built-in Xwayland `$DISPLAY`).
- After testing, mention where you could and couldn't reproduce, as well as the exact steps to reproduce if the issue is missing them.
- Try to reproduce the issue on your own system and write if you could or couldn't reproduce it.
- Upvote issues with a thumbs up reaction as you like.
- Ideas and feature requests from new users should go to Discussions.
If your issue is a duplicate, or not a niri issue (application bug, hardware problem, configuration problem), then please close it.
## Reviewing and testing pull requests
With the growing popularity, the volume of pull requests is honestly more than I can manage myself in my free time.
I would really appreciate help with testing and reviewing them.
### Testing
Pick a pull request you like, then build it and give it a go.
The [Developing niri wiki page](https://niri-wm.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
Be really thorough with your testing.
We're striving for polished features in niri, so point out any issues and bugs, even small ones like animation jank.
- Think of weird edge cases or unexpected interactions and try them to see that they work reasonably.
- Try to break the feature and check that it behaves well.
- Where applicable, try different input devices: keyboard, mouse, trackpad, tablet, touchscreen.
- Watch out for any new performance drops.
For bug fixes, first make sure you can reproduce the bug, then do the same steps in the PR test build, and verify that the bug is fixed.
Be similarly thorough: test any similar or related edge cases to verify that the fix doesn't introduce any new problems.
Write your findings in the pull request: any issues you found, or if everything worked well.
Re-test after the author updates the code to see that your issues were fixed.
Don't hesitate to test even if someone else already did; very frequently different people will stumble upon different problems.
### Reviewing
Reviewing pull requests is something I need the most help with since there are a lot of them, and it's quite time-consuming.
Anyone with code accepted into niri is welcome, but this is not a requirement; even if you aren't familiar with Rust you may find some logic problems.
Pick a pull request, then review its code.
- Check that everything looks good, check various conditions for edge cases.
- See if there are any scenarios the author forgot to handle.
- Check that the code fits well into the rest of niri, follows its design and code style.
- I understand this is vague. The idea is: look at the surrounding code and at similar modules (e.g. when implementing a new protocol, check other protocol implementations), and try to follow the style and structure.
- Check for unrelated changes that may be better split into their own pull request.
- Check that the wiki had been updated if necessary (for example, new config options were documented with examples, and have a correct Since annotation).
Point out everything you find as review comments (don't forget to submit the review).
Be constructive and respectful; some people may be new to programming and Rust.
As the author addresses the comments and issues, check the code again to see that the problems were fixed.
If everything looks good, say that, so I know someone has reviewed the PR.
As with testing, don't hesitate to look through and comment even if someone else already had.
Extra pairs of eyes catch more problems.
## Writing pull requests
When creating pull requests, please keep the following in mind.
- Make sure new features align with niri's design directions. Ideally, there should be an existing issue or discussion where we settled on that solution.
- Keep pull requests focused on a single feature or bug fix with no unrelated changes.
- Try to split your changes into small, self-contained commits. Every commit should build and pass tests. This makes it much easier to review your PR, and bisect for regressions in the future.
- When addressing PR comments, try to squash the changes straight into the relevant commits.
- In some cases when the requested changes are big/unclear, you can leave them as separate commits on top, but please squash and otherwise clean up the history when the changes are finalized.
- To update the main branch, please rebase instead of merging. Try to force-push the main update rebase separately from other changes, this way it's easy to skip during review since it's usually not interesting.
- When working on bigger features, I usually start with a big messy commit, then gradually split out smaller self-contained changes from it as the code gets into shape.
- [git-rebase.io](https://git-rebase.io/) is a helpful guide for splitting commits and cleaning up history in git.
- When you address a review comment, mark it as resolved.
- Remember to [run tests](https://niri-wm.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
- For new layout actions, remember to add them to the randomized tests. For weird Wayland handling, adding client-server tests in `src/tests/` could be very useful.
- Test your changes by hand thoroughly, including for edge cases and weird interactions. See the Testing section above for some tips.
- Remember to document new config options on the wiki.
- When opening a pull request, ensure "Allow edits from maintainers" is enabled, so I can make final tweaks before merging.
### How to get your pull request reviewed more quickly
- Make it small and self-contained. Avoid mixing several unrelated changes in one PR.
- Split the PR into small and self-contained commits. This makes it much easier to review.
- Discuss new features, options, or behavior changes beforehand; make sure there's consensus about the design.
- When creating the pull request, clearly write what it does, what problem it solves, how to test it.
- Follow the rest of the advice from this document.
## AI contributions
If you use LLMs for your contribution (issue, comment, pull request), then it is *your job* to check and clean up its output, just like with any other tool.
*You* have to spend the time doing this.
Particularly:
- If I can tell that a pull request is mostly LLM-generated, then very likely this pull request will take *significantly more time and effort* than usual to review and finish. This is based on my prior review experience. Therefore, I'm not interested in such pull requests—there's always plenty of human-written ones which take priority.
- When using an LLM to prepare an issue, the text usually has a lot of unnecessary wording and irrelevant details. Anyone looking at such an issue will quickly lose interest in reading through it (myself certainly). Clean up the text and keep only those details that actually matter.
- When using an LLM to comment on an issue, *you* have to verify that the comment makes sense, contributes something useful, and doesn't have unnecessary repetition.
[cosmic-comp]: https://github.com/pop-os/cosmic-comp
[anvil]: https://github.com/Smithay/smithay/tree/master/anvil
[xwayland-satellite]: https://github.com/Supreeeme/xwayland-satellite
Generated
+1294 -1318
View File
File diff suppressed because it is too large Load Diff
+51 -64
View File
@@ -6,35 +6,34 @@ members = [
]
[workspace.package]
version = "26.4.0"
version = "25.2.0"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/niri-wm/niri"
rust-version = "1.85"
repository = "https://github.com/YaLTeR/niri"
rust-version = "1.80.1"
[workspace.dependencies]
anyhow = "1.0.102"
bitflags = "2.11.1"
clap = { version = "4.6.1", features = ["derive"] }
insta = "1.47.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tracing = { version = "0.1.44", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tracy-client = { version = "0.18.4", default-features = false }
anyhow = "1.0.97"
bitflags = "2.9.0"
clap = { version = "4.5.34", features = ["derive"] }
insta = "1.42.2"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracy-client = { version = "0.18.0", default-features = false }
[workspace.dependencies.smithay]
# version = "0.4.1"
git = "https://github.com/Smithay/smithay.git"
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
# path = "../smithay"
default-features = false
[workspace.dependencies.smithay-drm-extras]
# version = "0.1.0"
git = "https://github.com/Smithay/smithay.git"
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
# path = "../smithay/smithay-drm-extras"
[package]
@@ -51,53 +50,48 @@ readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
# accesskit_unix 0.18 has a regression where it doesn't work in normal configurations.
# accesskit 0.21 is its correct dependent version.
# https://github.com/niri-wm/niri/issues/3594
accesskit = { version = "0.21", optional = true }
accesskit_unix = { version = "0.17", optional = true }
anyhow.workspace = true
arrayvec = "0.7.6"
async-channel = "2.5.0"
async-io = { version = "2.6.0", optional = true }
atomic = "0.6.1"
async-channel = "2.3.1"
async-io = { version = "2.4.0", optional = true }
atomic = "0.6.0"
bitflags.workspace = true
bytemuck = { version = "1.25.0", features = ["derive"] }
calloop = { version = "0.14.4", features = ["executor", "futures-io", "signals"] }
bytemuck = { version = "1.22.0", features = ["derive"] }
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
clap_complete = "4.6.2"
clap_complete_nushell = "4.6.0"
clap_complete = "4.5.47"
directories = "6.0.0"
drm-ffi = "0.9.1"
fastrand = "2.4.1"
futures-util = { version = "0.3.32", default-features = false, features = ["std", "io"] }
drm-ffi = "0.9.0"
fastrand = "2.3.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.32.1"
input = { version = "0.10.0", features = ["libinput_1_21"] }
glam = "0.30.1"
input = { version = "0.9.1", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.185"
libdisplay-info = "0.3.0"
log = { version = "0.4.29", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "26.4.0", path = "niri-config" }
niri-ipc = { version = "26.4.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.3.0"
pango = { version = "0.21.5", features = ["v1_44"] }
pangocairo = "0.21.5"
pipewire = { version = "0.9.2", optional = true, features = ["v0_3_33"] }
png = "0.18.1"
profiling = "1.0.17"
sd-notify = "0.5.0"
libc = "0.2.171"
libdisplay-info = "0.2.2"
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.2.0", path = "niri-config" }
niri-ipc = { version = "25.2.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.0.0"
pango = { version = "0.20.9", features = ["v1_44"] }
pangocairo = "0.20.7"
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
png = "0.17.16"
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
profiling = "1.0.16"
sd-notify = "0.4.5"
serde.workspace = true
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
wayland-backend = "0.3.15"
wayland-scanner = "0.31.10"
wayland-server = "0.31.13"
xcursor = "0.3.10"
zbus = { version = "5.13.2", optional = true }
url = { version = "2.5.4", optional = true }
wayland-backend = "0.3.8"
wayland-scanner = "0.31.6"
xcursor = "0.3.8"
zbus = { version = "5.5.0", optional = true }
[dependencies.smithay]
workspace = true
@@ -119,27 +113,24 @@ features = [
[dev-dependencies]
approx = "0.5.1"
calloop-wayland-source = "0.4.1"
calloop-wayland-source = "0.4.0"
insta.workspace = true
proptest = "1.11.0"
proptest-derive = { version = "0.8.0", features = ["boxed_union"] }
rayon = "1.12.0"
wayland-client = "0.31.14"
proptest = "1.6.0"
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
rayon = "1.10.0"
wayland-client = "0.31.8"
xshell = "0.2.7"
[build-dependencies]
pkg-config = "0.3.33"
[features]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, accessibility tree, power button handling).
dbus = ["dep:zbus", "dep:async-io", "dep:accesskit", "dep:accesskit_unix"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
xdp-gnome-screencast = ["dbus", "pipewire"]
# Enables the Tracy profiler instrumentation.
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default", "smithay/tracy_gpu_profiling"]
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
# Enables the on-demand Tracy profiler instrumentation.
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
# Enables Tracy allocation profiling.
@@ -147,10 +138,6 @@ profile-with-tracy-allocations = ["profile-with-tracy"]
# Enables dinit integration (global environment).
dinit = []
[lints.clippy]
new_without_default = "allow"
collapsible_match = "allow"
[profile.release]
debug = "line-tables-only"
overflow-checks = true
@@ -165,7 +152,7 @@ insta.opt-level = 3
similar.opt-level = 3
[package.metadata.generate-rpm]
version = "26.04"
version = "25.02"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+33 -48
View File
@@ -1,16 +1,16 @@
<h1 align="center"><img alt="niri" src="https://github.com/user-attachments/assets/07d05cd0-d5dc-4a28-9a35-51bae8f119a0"></h1>
<h1 align="center">niri</h1>
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
<a href="https://github.com/niri-wm/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/niri-wm/niri"></a>
<a href="https://github.com/niri-wm/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/niri-wm/niri?logo=github"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
<p align="center">
<a href="https://niri-wm.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://niri-wm.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/niri-wm/niri/discussions/325">Setup&nbsp;Showcase</a>
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![niri with a few windows open](https://github.com/user-attachments/assets/535e6530-2f44-4b84-a883-1240a3eee6e9)
![niri with a few windows open](https://github.com/user-attachments/assets/d142e57d-a25d-4ddb-ab46-311417458211)
## About
@@ -29,25 +29,20 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Built from the ground up for scrollable tiling
- [Dynamic workspaces](https://niri-wm.github.io/niri/Workspaces.html) like in GNOME
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://niri-wm.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://niri-wm.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/niri-wm/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/niri-wm/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://niri-wm.github.io/niri/Tabs.html)
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://niri-wm.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
- [Animations](https://github.com/niri-wm/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/niri-wm/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- Live-reloading config
- Works with [screen readers](https://niri-wm.github.io/niri/Accessibility.html)
## Video Demo
https://github.com/niri-wm/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
Also check out this video from Brodie Robertson that showcases a lot of the niri functionality: [Niri Is My New Favorite Wayland Compositor](https://youtu.be/DeYx2exm04M)
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
## Status
@@ -55,9 +50,8 @@ Niri is stable for day-to-day use and does most things expected of a Wayland com
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
Give it a try!
Follow the instructions on the [Getting Started](https://niri-wm.github.io/niri/Getting-Started.html) page.
Follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
Also check out [awesome-niri], a list of niri-related links and projects.
Here are some points you may have questions about:
@@ -72,28 +66,14 @@ We have touchpad gestures, but no touchscreen gestures yet.
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
- **Xwayland**: [integrated](https://niri-wm.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
## Media
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T) · *December 2024*
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
[An interview with Ivan, the developer behind Niri](https://www.trommelspeicher.de/podcast/special_the_developer_behind_niri) · *June 2025*
An interview by a German tech podcast Das Triumvirat (in English).
We talk about niri development and history, and my experience building and maintaining niri.
[A tour of the niri scrolling-tiling Wayland compositor](https://lwn.net/Articles/1025866/) · *July 2025*
An LWN article with a nice overview and introduction to niri.
## Contributing
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
- Steam and games, including Proton: work perfectly through xwayland-satellite.
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
- Discord and other Electron apps: work well through xwayland-satellite.
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
- Display scaling (integer or fractional) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
For games, you can run them in [gamescope] at native resolution, even with display scaling.
## Inspiration
@@ -108,24 +88,29 @@ Here are some other projects which implement a similar workflow:
- [PaperWM]: scrollable tiling on top of GNOME Shell.
- [karousel]: scrollable tiling on top of KDE.
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [papersway]: scrollable tiling on top of sway/i3.
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
## Media
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
## Contact
Our main communication channel is a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
We also have a community Discord server: https://discord.gg/vT8Sfjy7sx
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
[PaperWM]: https://github.com/paperwm/PaperWM
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
[awesome-niri]: https://github.com/niri-wm/awesome-niri
[karousel]: https://github.com/peterfajdiga/karousel
[papersway]: https://spwhitton.name/tech/code/papersway/
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
[hyprscroller]: https://github.com/dawsers/hyprscroller
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
[OpenTabletDriver]: https://opentabletdriver.net/
[gamescope]: https://github.com/ValveSoftware/gamescope
-10
View File
@@ -1,10 +0,0 @@
fn main() {
println!("cargo:rustc-check-cfg=cfg(have_libinput_plugin_system)");
if pkg_config::Config::new()
.atleast_version("1.30.0")
.probe("libinput")
.is_ok()
{
println!("cargo:rustc-cfg=have_libinput_plugin_system")
}
}
-2
View File
@@ -1,2 +0,0 @@
site
__pycache__
-19
View File
@@ -1,19 +0,0 @@
from __future__ import annotations
import re
# todo: this could be done generically, so that any
# ```language,annotation,anything-else
# is reduced to
# ```language
# which is what's supported by mkdocs/pygments
# also note: mkdocs provides ways to highlight lines, add line numbers
# but these are added as
# ```language linenums="1"
# and not split by comma
def on_page_markdown(
markdown: str, *, page, config, files
):
return re.sub(
r",must-fail",
'', markdown, flags = re.I | re.M
)
-44
View File
@@ -1,44 +0,0 @@
# Copyright (c) 2016-2025 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from __future__ import annotations
import re
from re import Match
def on_page_markdown(
markdown: str, *, page, config, files
):
def replace(match: Match):
matches = match.groups()
preposition, version = matches[0], matches[1]
return _badge_for_version(preposition, version)
return re.sub(
r"<sup>(Until|Since): (.*?)</sup>",
replace, markdown, flags = re.I | re.M
)
def _badge_for_version(preposition: str, version: str):
if version == "next release":
# we might fail to make real links to release notes on other cases too, but for now this is the one i've found
return f"<span class=\"badge\">{preposition}: {version}</span>"
else:
path = f"https://github.com/niri-wm/niri/releases/tag/v{version}"
return f"<span class=\"badge\">[{preposition}: {version}]({path})</span>"
-116
View File
@@ -1,116 +0,0 @@
site_name: niri
docs_dir: wiki
site_url: https://niri-wm.github.io/niri
repo_url: https://github.com/niri-wm/niri
edit_uri: edit/main/docs/wiki/
use_directory_urls: false
theme:
name: material
logo: _assets/icons/logo.svg
favicon: _assets/icons/logo.svg
features:
- navigation.instant
- search.suggest
- content.code.copy
- content.action.edit
palette:
- media: "(prefers-color-scheme)"
primary: custom
toggle:
icon: material/brightness-auto
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
primary: custom
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
primary: custom
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to system preference
markdown_extensions:
- github-callouts
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.keys
- toc:
permalink: '#'
plugins:
- search
hooks:
- hooks/shortcodes.py
- hooks/remove-must-fail.py
extra_css:
- _assets/stylesheets/niri.css
strict: true
validation:
nav:
omitted_files: warn
not_found: warn
absolute_links: relative_to_docs
links:
not_found: warn
anchors: warn
absolute_links: relative_to_docs
unrecognized_links: warn
not_in_nav: |
_Sidebar.md
Configuration:-Overview.md
README.md
# ah, wouldn't it be nice if we could autogenerate this with wiki/_Sidebar.md
nav:
- Usage:
- Getting Started: Getting-Started.md
- Example systemd Setup: Example-systemd-Setup.md
- Important Software: Important-Software.md
- Workspaces: Workspaces.md
- Floating Windows: Floating-Windows.md
- Tabs: Tabs.md
- Overview: Overview.md
- Screencasting: Screencasting.md
- LayerShell Components: LayerShell-Components.md
- IPC, niri msg: IPC.md
- Application-Specific Issues: Application-Issues.md
- Nvidia: Nvidia.md
- Xwayland: Xwayland.md
- Gestures: Gestures.md
- Fullscreen and Maximize: Fullscreen-and-Maximize.md
- Window Effects: Window-Effects.md
- Packaging niri: Packaging-niri.md
- Integrating niri: Integrating-niri.md
- Accessibility: Accessibility.md
- Name and Logo: Name-and-Logo.md
- FAQ: FAQ.md
- Configuration:
- Introduction: Configuration:-Introduction.md
- Input: Configuration:-Input.md
- Outputs: Configuration:-Outputs.md
- Key Bindings: Configuration:-Key-Bindings.md
- Switch Events: Configuration:-Switch-Events.md
- Layout: Configuration:-Layout.md
- Named Workspaces: Configuration:-Named-Workspaces.md
- Miscellaneous: Configuration:-Miscellaneous.md
- Window Rules: Configuration:-Window-Rules.md
- Layer Rules: Configuration:-Layer-Rules.md
- Animations: Configuration:-Animations.md
- Gestures: Configuration:-Gestures.md
- Recent Windows: Configuration:-Recent-Windows.md
- Debug Options: Configuration:-Debug-Options.md
- Include: Configuration:-Include.md
- Development:
- Design Principles: Development:-Design-Principles.md
- Developing niri: Development:-Developing-niri.md
- Documenting niri: Development:-Documenting-niri.md
- Fractional Layout: Development:-Fractional-Layout.md
- Redraw Loop: Development:-Redraw-Loop.md
- Animation Timing: Development:-Animation-Timing.md
-14
View File
@@ -1,14 +0,0 @@
[project]
name = "niri-docs"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"markdown-callouts>=0.4.0",
"mkdocs-material>=9.6.15",
"pygments",
]
# for KDL highlighting support
# FIXME: use the official pygments package once https://github.com/pygments/pygments/pull/2936 is merged
[tool.uv.sources]
pygments = { git = "https://github.com/chinatsu/pygments", rev = "0f0b0d4da2839e1285881389155bb4605a0a6dc4" }
Generated
-511
View File
@@ -1,511 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "backrefs"
version = "5.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" },
{ url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" },
{ url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" },
{ url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" },
{ url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" },
]
[[package]]
name = "certifi"
version = "2025.7.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" },
{ url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" },
{ url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" },
{ url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" },
{ url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" },
{ url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" },
{ url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" },
{ url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" },
{ url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
{ url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
{ url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
{ url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
{ url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
{ url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
{ url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "ghp-import"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markdown"
version = "3.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" },
]
[[package]]
name = "markdown-callouts"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/73/ae5aa379f6f7fea9d0bf4cba888f9a31d451d90f80033ae60ae3045770d5/markdown_callouts-0.4.0.tar.gz", hash = "sha256:7ed2c90486967058a73a547781121983839522d67041ae52c4979616f1b2b746", size = 9768, upload-time = "2024-01-22T23:18:18.513Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/b5/7b0a0a52c82bfccd830af2a8cc8add1c5bc932e0204922434954a631dd51/markdown_callouts-0.4.0-py3-none-any.whl", hash = "sha256:ed0da38f29158d93116a0d0c6ecaf9df90b37e0d989b5337d678ee6e6d6550b7", size = 7108, upload-time = "2024-01-22T23:18:17.465Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
{ url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" },
{ url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" },
{ url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" },
{ url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" },
{ url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" },
{ url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" },
{ url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
{ url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
{ url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
{ url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
{ url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
{ url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
{ url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
{ url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
{ url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
{ url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
[[package]]
name = "mergedeep"
version = "1.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
]
[[package]]
name = "mkdocs"
version = "1.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "ghp-import" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "markupsafe" },
{ name = "mergedeep" },
{ name = "mkdocs-get-deps" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "pyyaml" },
{ name = "pyyaml-env-tag" },
{ name = "watchdog" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
]
[[package]]
name = "mkdocs-get-deps"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mergedeep" },
{ name = "platformdirs" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" },
]
[[package]]
name = "mkdocs-material"
version = "9.6.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "babel" },
{ name = "backrefs" },
{ name = "colorama" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "mkdocs" },
{ name = "mkdocs-material-extensions" },
{ name = "paginate" },
{ name = "pygments" },
{ name = "pymdown-extensions" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/30/dda19f0495a9096b64b6b3c07c4bfcff1c76ee0fc521086d53593f18b4c0/mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a", size = 8716840, upload-time = "2025-07-01T10:14:13.18Z" },
]
[[package]]
name = "mkdocs-material-extensions"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
]
[[package]]
name = "niri-docs"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "markdown-callouts" },
{ name = "mkdocs-material" },
{ name = "pygments" },
]
[package.metadata]
requires-dist = [
{ name = "markdown-callouts", specifier = ">=0.4.0" },
{ name = "mkdocs-material", specifier = ">=9.6.15" },
{ name = "pygments", git = "https://github.com/chinatsu/pygments?rev=0f0b0d4da2839e1285881389155bb4605a0a6dc4" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "paginate"
version = "0.5.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "platformdirs"
version = "4.3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { git = "https://github.com/chinatsu/pygments?rev=0f0b0d4da2839e1285881389155bb4605a0a6dc4#0f0b0d4da2839e1285881389155bb4605a0a6dc4" }
[[package]]
name = "pymdown-extensions"
version = "10.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
{ url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
{ url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
{ url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
{ url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
{ url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
{ url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
{ url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[package]]
name = "pyyaml-env-tag"
version = "1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
{ url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
{ url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
{ url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
{ url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
{ url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
{ url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
{ url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
{ url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
{ url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]
-45
View File
@@ -1,45 +0,0 @@
## Screen readers
<sup>Since: 25.08</sup>
Niri has basic support for screen readers (specifically, [Orca](https://orca.gnome.org)) when running as a full desktop session, i.e. you need to start niri through a display manager or through `niri-session`.
To avoid conflicts with an already running compositor, niri won't expose accessibility interfaces when started as a nested window, or as a plain `/usr/bin/niri` on a TTY.
We implement the `org.freedesktop.a11y.KeyboardMonitor` D-Bus interface for Orca to listen and grab keyboard keys, and we expose the main niri UI elements via [AccessKit](https://accesskit.dev).
Specifically, niri will announce:
- workspace switching, for example it'll say "Workspace 2" when you switch to the second workspace;
- the exit confirmation dialog (appears on <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd> by default);
- <sup>Since: 25.11</sup> niri has an <kbd>Alt</kbd><kbd>Tab</kbd> window switcher where it will announce the selected window title;
- entering the screenshot UI and the overview (niri will say when these are focused, nothing else for now);
- whenever a config parse error occurs;
- the important hotkeys list (for now, as one big announcement without tab navigation; appears on <kbd>Super</kbd><kbd>Shift</kbd><kbd>/</kbd> by default).
Here's a demo video, watch with sound on.
<video controls src="https://github.com/user-attachments/assets/afceba6f-79f1-47ec-b859-a0fcb7f8eae3">
https://github.com/user-attachments/assets/afceba6f-79f1-47ec-b859-a0fcb7f8eae3
</video>
Make sure [Xwayland](./Xwayland.md) works, then run `orca`.
The default config binds <kbd>Super</kbd><kbd>Alt</kbd><kbd>S</kbd> to toggle Orca, which is the standard key binding.
Note that there are some limitations:
- We don't have a bind to move focus to layer-shell panels. This is not hard to add, but it would be good to have some consensus or prior art with LXQt/Xfce on how exactly this should work.
- You need to have a screen connected and enabled. Without a screen, niri won't give focus any window. This makes sense for sighted users, and I'm not entirely sure what makes the most sense for accessibility purposes (maybe, it'd be better solved with virtual monitors).
- You need working EGL (hardware acceleration).
- We don't have screen curtain functionality yet.
If you're shipping niri and would like to make it work better for screen readers out of the box, consider the following changes to the default niri config:
- Change the default terminal from Alacritty to one that supports screen readers. For example, [GNOME Console](https://gitlab.gnome.org/GNOME/console) or [GNOME Terminal](https://gitlab.gnome.org/GNOME/gnome-terminal) should work well.
- Change the default application launcher and screen locker to ones that support screen readers. For example, [xfce4-appfinder](https://docs.xfce.org/xfce/xfce4-appfinder/start) is an accessible launcher. Suggestions welcome! Likely, something GTK-based will work fine.
- Add some [`spawn-at-startup`](./Configuration:-Miscellaneous.md#spawn-at-startup) command that plays a sound which will indicate to users that niri has finished loading.
- Add `spawn-at-startup "orca"` to run Orca automatically at niri startup.
## Desktop zoom
There's no built-in zoom yet, but you can use third-party utilities like [wooz](https://github.com/negrel/wooz).
-115
View File
@@ -1,115 +0,0 @@
### Overview
<sup>Since: 25.02</sup>
The `gestures` config section contains gesture settings.
For an overview of all niri gestures, see the [Gestures](./Gestures.md) wiki page.
Here's a quick glance at the available settings along with their default values.
```kdl
gestures {
dnd-edge-view-scroll {
trigger-width 30
delay-ms 100
max-speed 1500
}
dnd-edge-workspace-switch {
trigger-height 50
delay-ms 100
max-speed 1500
}
hot-corners {
// off
top-left
// top-right
// bottom-left
// bottom-right
}
}
```
### `dnd-edge-view-scroll`
Scroll the tiling view when moving the mouse cursor against a monitor edge during drag-and-drop (DnD).
Also works on a touchscreen.
This will work for regular drag-and-drop (e.g. dragging a file from a file manager), and for window interactive move when targeting the tiling layout.
The options are:
- `trigger-width`: size of the area near the monitor edge that will trigger the scrolling, in logical pixels.
- `delay-ms`: delay in milliseconds before the scrolling starts.
Avoids unwanted scrolling when dragging things across monitors.
- `max-speed`: maximum scrolling speed in logical pixels per second.
The scrolling speed increases linearly as you move your mouse cursor from `trigger-width` to the very edge of the monitor.
```kdl
gestures {
// Increase the trigger area and maximum speed.
dnd-edge-view-scroll {
trigger-width 100
max-speed 3000
}
}
```
### `dnd-edge-workspace-switch`
<sup>Since: 25.05</sup>
Scroll the workspaces up/down when moving the mouse cursor against a monitor edge during drag-and-drop (DnD) while in the overview.
Also works on a touchscreen.
The options are:
- `trigger-height`: size of the area near the monitor edge that will trigger the scrolling, in logical pixels.
- `delay-ms`: delay in milliseconds before the scrolling starts.
Avoids unwanted scrolling when dragging things across monitors.
- `max-speed`: maximum scrolling speed; 1500 corresponds to one screen height per second.
The scrolling speed increases linearly as you move your mouse cursor from `trigger-width` to the very edge of the monitor.
```kdl
gestures {
// Increase the trigger area and maximum speed.
dnd-edge-workspace-switch {
trigger-height 100
max-speed 3000
}
}
```
### `hot-corners`
<sup>Since: 25.05</sup>
Put your mouse at the very top-left corner of a monitor to toggle the overview.
Also works during drag-and-dropping something.
`off` disables the hot corners.
```kdl
// Disable the hot corners.
gestures {
hot-corners {
off
}
}
```
<sup>Since: 25.11</sup> You can choose specific hot corners by name: `top-left`, `top-right`, `bottom-left`, `bottom-right`.
If no corners are explicitly set, the top-left corner will be active by default.
```kdl
// Enable the top-right and bottom-right hot corners.
gestures {
hot-corners {
top-right
bottom-right
}
}
```
You can also customize hot corners per-output [in the output config](./Configuration:-Outputs.md#hot-corners).
-300
View File
@@ -1,300 +0,0 @@
<sup>Since: 25.11</sup>
You can include other files at the top level of the config.
```kdl,must-fail
// Some settings...
include "colors.kdl"
// Some more settings...
```
Included files have the same structure as the main config file.
Settings from included files will be merged with the settings from the main config file.
Included config files can in turn include more files.
All included files are watched for changes, and the config live-reloads when any of them change.
You can include by filename or path.
* Relative to the current file: `other.kdl` or `./other.kdl`
* By absolute path: `/path/to/file.kdl`
* <sup>Since: 26.04</sup> Home dir paths: `~/file.kdl` expands to `/home/user/file.kdl`
Includes work only at the top level of the config:
```kdl,must-fail
// All good: include at the top level.
include "something.kdl"
layout {
// NOT allowed: include inside some other section.
include "other.kdl"
}
```
### Positionality
Includes are *positional*.
They will override options set *prior* to them.
Window rules from included files will be inserted at the position of the `include` line.
For example:
```kdl
// colors.kdl
layout {
border {
active-color "green"
}
}
overview {
backdrop-color "green"
}
```
```kdl,must-fail
// config.kdl
layout {
border {
active-color "red"
}
}
// This overrides the border color and the backdrop color to green.
include "colors.kdl"
// This sets the overview backdrop color to red again.
overview {
backdrop-color "red"
}
```
The end result:
- the border color is green (from `colors.kdl`),
- the overview backdrop color is red (it was set *after* `colors.kdl`).
Another example:
```kdl
// rules.kdl
window-rule {
match app-id="Alacritty"
open-maximized false
}
```
```kdl,must-fail
// config.kdl
window-rule {
open-maximized true
}
// Window rules get inserted at this position.
include "rules.kdl"
window-rule {
match app-id="firefox$"
open-maximized true
}
```
This is equivalent to the following config file:
```kdl
window-rule {
open-maximized true
}
// Included from rules.kdl.
window-rule {
match app-id="Alacritty"
open-maximized false
}
window-rule {
match app-id="firefox$"
open-maximized true
}
```
### Optional includes
<sup>Since: 26.04</sup>
By default, including a nonexistent file will cause an error.
You can allow nonexistent includes by setting `optional=true`:
```kdl,must-fail
// Won't fail if this file doesn't exist.
include optional=true "optional-config.kdl"
// Regular include, will fail if the file doesn't exist.
include "required-config.kdl"
```
When an optional include file is missing, niri will emit a warning in the logs on every config reload.
This reminds you that the file is missing while still loading the config successfully.
The optional file is still watched for changes, so if you create it later, the config will automatically reload and apply the new settings.
Note that `optional` only affects whether a missing file causes an error.
If the file exists but contains invalid syntax or other errors, those errors will still cause a parsing failure.
### Merging
Most config sections are merged between includes, meaning that you can set only a few properties, and only those properties will change.
```kdl
// colors.kdl
layout {
// Does not affect gaps, border width, etc.
// Only changes colors as written.
focus-ring {
active-color "blue"
}
border {
active-color "green"
}
}
```
```kdl,must-fail
// config.kdl
include "colors.kdl"
layout {
// Does not set border and focus-ring colors,
// so colors from colors.kdl are used.
gaps 8
border {
width 8
}
}
```
#### Multipart sections
Multipart sections like `window-rule`, `output`, or `workspace` are inserted as is without merging:
```kdl
// laptop.kdl
output "eDP-1" {
// ...
}
```
```kdl,must-fail
// config.kdl
output "DP-2" {
// ...
}
include "laptop.kdl"
// End result: both DP-2 and eDP-1 settings.
```
#### Binds
`binds` will override previously-defined conflicting keys:
```kdl
// binds.kdl
binds {
Mod+T { spawn "alacritty"; }
}
```
```kdl,must-fail
// config.kdl
include "binds.kdl"
binds {
// Overrides Mod+T from binds.kdl.
Mod+T { spawn "foot"; }
}
```
#### Flags
Most flags can be disabled with `false`:
```kdl
// csd.kdl
// Write "false" to explicitly disable.
prefer-no-csd false
```
```kdl,must-fail
// config.kdl
// Enable prefer-no-csd in the main config.
prefer-no-csd
// Including csd.kdl will disable it again.
include "csd.kdl"
```
#### Non-merging sections
Some sections where the contents represent a combined structure are not merged.
Examples are `struts`, `preset-column-widths`, individual subsections in `animations`, pointing device sections in `input`.
```kdl
// struts.kdl
layout {
struts {
left 64
right 64
}
}
```
```kdl,must-fail
// config.kdl
layout {
struts {
top 64
bottom 64
}
}
include "struts.kdl"
// Struts are not merged.
// End result is only left and right struts.
```
### Border special case
There's one special case that differs between the main config and included configs.
Writing `layout { border {} }` in an included config does nothing (since no properties are changed).
However, writing the same in the main config will *enable* the border, i.e. it's equivalent to `layout { border { on; } }`.
So, if you want to move your layout configuration from the main config to a separate file, remember to add `on` to the border section, for example:
```kdl
// separate.kdl
layout {
border {
// Add this line:
on
width 4
active-color "#ffc87f"
inactive-color "#505050"
}
}
```
The reason for this special case is that this is how it historically worked: back when I added borders, we didn't have any `on` flags, so I made writing the `border {}` section enable the border, with an explicit `off` to disable it.
It wouldn't be too problematic to change it, however the default config always had a pre-filled `layout { border { off; } }` section with a note saying that commenting out the `off` is enough to enable the border.
Many people likely have this part of the default config embedded in their configs now, so changing how it works would just cause a lot of confusion.
-409
View File
@@ -1,409 +0,0 @@
This page documents all top-level options that don't otherwise have dedicated pages.
Here are all of these options at a glance:
```kdl
spawn-at-startup "waybar"
spawn-at-startup "alacritty"
spawn-sh-at-startup "qs -c ~/source/qs/MyAwesomeShell"
prefer-no-csd
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
environment {
QT_QPA_PLATFORM "wayland"
DISPLAY null
}
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 48
hide-when-typing
hide-after-inactive-ms 1000
}
overview {
zoom 0.5
backdrop-color "#262626"
workspace-shadow {
// off
softness 40
spread 10
offset x=0 y=10
color "#00000050"
}
}
xwayland-satellite {
// off
path "xwayland-satellite"
}
clipboard {
disable-primary
}
hotkey-overlay {
skip-at-startup
hide-not-bound
}
config-notification {
disable-failed
}
blur {
// off
passes 3
offset 3.0
noise 0.02
saturation 1.5
}
```
### `spawn-at-startup`
Add lines like this to spawn processes at niri startup.
`spawn-at-startup` accepts a path to the program binary as the first argument, followed by arguments to the program.
This option works the same way as the [`spawn` key binding action](./Configuration:-Key-Bindings.md#spawn), so please read about all its subtleties there.
```kdl
spawn-at-startup "waybar"
spawn-at-startup "alacritty"
```
Note that running niri as a systemd session supports xdg-desktop-autostart out of the box, which may be more convenient to use.
Thanks to this, apps that you configured to autostart in GNOME will also "just work" in niri, without any manual `spawn-at-startup` configuration.
### `spawn-sh-at-startup`
<sup>Since: 25.08</sup>
Add lines like this to run shell commands at niri startup.
The argument is a single string that is passed verbatim to `sh`.
You can use shell variables, pipelines, `~` expansion and everything else as expected.
See detailed description in the docs for the [`spawn-sh` key binding action](./Configuration:-Key-Bindings.md#spawn-sh).
```kdl
// Pass all arguments in the same string.
spawn-sh-at-startup "qs -c ~/source/qs/MyAwesomeShell"
```
### `prefer-no-csd`
This flag will make niri ask the applications to omit their client-side decorations.
If an application will specifically ask for CSD, the request will be honored.
Additionally, clients will be informed that they are tiled, removing some rounded corners.
With `prefer-no-csd` set, applications that negotiate server-side decorations through the xdg-decoration protocol will have focus ring and border drawn around them *without* a solid colored background.
> [!NOTE]
> Unlike most other options, changing `prefer-no-csd` will not entirely affect already running applications.
> It will make some windows rectangular, but won't remove the title bars.
> This mainly has to do with niri working around a [bug in SDL2](https://github.com/libsdl-org/SDL/issues/8173) that prevents SDL2 applications from starting.
>
> Restart applications after changing `prefer-no-csd` in the config to fully apply it.
```kdl
prefer-no-csd
```
### `screenshot-path`
Set the path where screenshots are saved.
A `~` at the front will be expanded to the home directory.
The path is formatted with `strftime(3)` to give you the screenshot date and time.
Niri will create the last folder of the path if it doesn't exist.
```kdl
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
```
You can also set this option to `null` to disable saving screenshots to disk.
```kdl
screenshot-path null
```
### `environment`
Override environment variables for processes spawned by niri.
```kdl
environment {
// Set a variable like this:
// QT_QPA_PLATFORM "wayland"
// Remove a variable by using null as the value:
// DISPLAY null
}
```
Note that these variables do not propagate to the systemd global environment, so tools and applications started by systemd do not see them.
In particular, if you start a desktop shell like DankMaterialShell through systemd, then use its built-in application launcher, the apps won't see these environment variables.
If you want all processes to see the environment variables, you can set them in your login shell config instead (i.e. `~/.bash_profile`).
The `niri-session` shell script runs through the login shell and imports all environment variables to systemd before starting niri.
Keep in mind that all compositors will see variables set in the login shell, not just niri.
### `cursor`
Change the theme and size of the cursor as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE` environment variables.
```kdl
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 48
}
```
#### `hide-when-typing`
<sup>Since: 0.1.10</sup>
If set, hides the cursor when pressing a key on the keyboard.
> [!NOTE]
> This setting might interfere with games running in Wine in native Wayland mode that use mouselook, such as first-person games.
> If your character's point of view jumps down when you press a key and move the mouse simultaneously, try disabling this setting.
```kdl
cursor {
hide-when-typing
}
```
#### `hide-after-inactive-ms`
<sup>Since: 0.1.10</sup>
If set, the cursor will automatically hide once this number of milliseconds passes since the last cursor movement.
```kdl
cursor {
// Hide the cursor after one second of inactivity.
hide-after-inactive-ms 1000
}
```
### `overview`
<sup>Since: 25.05</sup>
Settings for the [Overview](./Overview.md).
#### `zoom`
Control how much the workspaces zoom out in the overview.
`zoom` ranges from 0 to 0.75 where lower values make everything smaller.
```kdl
// Make workspaces four times smaller than normal in the overview.
overview {
zoom 0.25
}
```
#### `backdrop-color`
Set the backdrop color behind workspaces in the overview.
The backdrop is also visible between workspaces when switching.
The alpha channel for this color will be ignored.
```kdl
// Make the backdrop light.
overview {
backdrop-color "#777777"
}
```
You can also set the color per-output [in the output config](./Configuration:-Outputs.md#backdrop-color).
#### `workspace-shadow`
Control the shadow behind workspaces visible in the overview.
Settings here mirror the normal [`shadow` config in the layout section](./Configuration:-Layout.md#shadow), so check the documentation there.
Workspace shadows are configured for a workspace size normalized to 1080 pixels tall, then zoomed out together with the workspace.
Practically, this means that you'll want bigger spread, offset, and softness compared to window shadows.
```kdl
// Disable workspace shadows in the overview.
overview {
workspace-shadow {
off
}
}
```
### `xwayland-satellite`
<sup>Since: 25.08</sup>
Settings for integration with [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
When a recent enough xwayland-satellite is detected, niri will create the X11 sockets and set `DISPLAY`, then automatically spawn `xwayland-satellite` when an X11 client tries to connect.
If Xwayland dies, niri will keep watching the X11 socket and restart `xwayland-satellite` as needed.
This is very similar to how built-in Xwayland works in other compositors.
`off` disables the integration: niri won't create an X11 socket and won't set the `DISPLAY` environment variable.
`path` sets the path to the `xwayland-satellite` binary.
By default, it's just `xwayland-satellite`, so it's looked up like any other non-absolute program name.
```kdl
// Use a custom build of xwayland-satellite.
xwayland-satellite {
path "~/source/rs/xwayland-satellite/target/release/xwayland-satellite"
}
```
### `clipboard`
<sup>Since: 25.02</sup>
Clipboard settings.
Set the `disable-primary` flag to disable the primary clipboard (middle-click paste).
Toggling this flag will only apply to applications started afterward.
```kdl
clipboard {
disable-primary
}
```
### `hotkey-overlay`
Settings for the "Important Hotkeys" overlay.
#### `skip-at-startup`
Set the `skip-at-startup` flag if you don't want to see the hotkey help at niri startup.
```kdl
hotkey-overlay {
skip-at-startup
}
```
#### `hide-not-bound`
<sup>Since: 25.08</sup>
By default, niri will show the most important actions even if they aren't bound to any key, to prevent confusion.
Set the `hide-not-bound` flag if you want to hide all actions not bound to any key.
```kdl
hotkey-overlay {
hide-not-bound
}
```
You can customize which binds the hotkey overlay shows using the [`hotkey-overlay-title` property](./Configuration:-Key-Bindings.md#custom-hotkey-overlay-titles).
### `config-notification`
<sup>Since: 25.08</sup>
Settings for the config created/failed notification.
Set the `disable-failed` flag to disable the "Failed to parse the config file" notification.
For example, if you have a custom one.
```kdl
config-notification {
disable-failed
}
```
### `blur`
<sup>Since: 26.04</sup>
Blur configuration that affects all background blur.
See the [window effects page](./Window-Effects.md) for an overview of background effects.
```kdl
// These are the default values:
blur {
// off
passes 3
offset 3
noise 0.02
saturation 1.5
}
```
#### `off`
By default, blur is available on request by a window or layer surface (via the `ext-background-effect` protocol).
You can also enable it manually with the `blur true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
Setting the `off` flag will disable all blur, both requested by the window, and configured in window rules.
```kdl
blur {
off
}
```
#### `passes` and `offset`
`passes` controls the number of downsample/upsample passes for dual kawase blur.
More passes produce a larger, smoother blur, but cost more GPU resources.
`offset` is the pixel offset multiplier for each pass.
Offset `1` is the original dual kawase blur.
Larger values produce a smoother blur, at no additional GPU cost.
However, setting `offset` too big will produce visual artifacts.
You will need to increase `passes` to be able to use a bigger `offset` without artifacts.
When configuring blur, try increasing `offset` first (since it doesn't cause any extra GPU load) until you start getting artifacts.
Then, if you still need smoother blur, increase `passes` by 1.
Keep doing this until you get the desired visuals.
```kdl
blur {
passes 3
offset 3.0
}
```
#### `noise`
Amount of noise to add on top of the blur.
This is helpful to reduce color banding artifacts.
```kdl
blur {
noise 0.02
}
```
#### `saturation`
Color saturation applied to the blurred background.
Values above `1` increase saturation; values below `1` reduce it.
```kdl
blur {
saturation 1.5
}
```
-1
View File
@@ -1 +0,0 @@
This wiki page has moved to: [Introduction](./Configuration:-Introduction.md).
-193
View File
@@ -1,193 +0,0 @@
### Overview
<sup>Since: 25.11</sup>
In this section you can configure the recent windows switcher (Alt-Tab).
Here is an outline of the available settings and their default values:
```kdl
recent-windows {
// off
debounce-ms 750
open-delay-ms 150
highlight {
active-color "#999999ff"
urgent-color "#ff9999ff"
padding 30
corner-radius 0
}
previews {
max-height 480
max-scale 0.5
}
binds {
Alt+Tab { next-window; }
Alt+Shift+Tab { previous-window; }
Alt+grave { next-window filter="app-id"; }
Alt+Shift+grave { previous-window filter="app-id"; }
Mod+Tab { next-window; }
Mod+Shift+Tab { previous-window; }
Mod+grave { next-window filter="app-id"; }
Mod+Shift+grave { previous-window filter="app-id"; }
}
}
```
`off` disables the recent windows switcher altogether.
### `debounce-ms`
Delay, in milliseconds, between the window receiving focus and getting "committed" to the recent windows list.
When you want to focus some window, you might end up focusing some unrelated windows on the way:
- with keyboard navigation, the windows between your current one and the target one;
- with [`focus-follows-mouse`](./Configuration:-Input.md#focus-follows-mouse), the windows you happen to cross with the mouse pointer on the way to the target window.
The debounce delay prevents those intermediate windows from polluting the recent windows list.
Note that some actions, like keyboard input into the target window, will skip this delay and commit the window to the list immediately.
This way, the recent windows list stays responsive while not getting polluted too much with unintended windows.
If you want windows to appear in recent windows right away, including intermediate windows, you can reduce the delay or set it to zero:
```kdl
recent-windows {
// Commit windows to the recent windows list as soon as they're focused,
// with no debounce delay.
debounce-ms 0
}
```
### `open-delay-ms`
Delay, in milliseconds, between pressing the Alt-Tab bind and the recent windows switcher visually appearing on screen.
The switcher is delayed by default so that quickly tapping Alt-Tab to switch windows wouldn't cause annoying fullscreen visual changes.
```kdl
recent-windows {
// Make the switcher appear instantly.
open-delay-ms 0
}
```
### `highlight`
Controls the highlight behind the focused window preview in the recent windows switcher.
- `active-color`: normal color of the focused window highlight.
- `urgent-color`: color of an urgent focused window highlight, also visible in a darker shade on unfocused windows.
- `padding`: padding of the highlight around the window preview, in logical pixels.
- `corner-radius`: corner radius of the highlight.
```kdl
recent-windows {
// Round the corners on the highlight.
highlight {
corner-radius 14
}
}
```
### `previews`
Controls the window previews in the switcher.
- `max-scale`: maximum scale of the window previews.
Windows cannot be scaled bigger than this value.
- `max-height`: maximum height of the window previews.
Further limits the size of the previews in order to occupy less space on large monitors.
On smaller monitors, the previews will be primarily limited by `max-scale`, and on larger monitors they will be primarily limited by `max-height`.
The `max-scale` limit is imposed twice: on the final window scale, and on the window height which cannot exceed `monitor height × max scale`.
```kdl
recent-windows {
// Make the previews smaller to fit more on screen.
previews {
max-height 320
}
}
```
```kdl
recent-windows {
// Make the previews larger to see the window contents.
previews {
max-height 1080
max-scale 0.75
}
}
```
### `binds`
Configure binds that open and navigate the recent windows switcher.
The defaults are <kbd>Alt</kbd><kbd>Tab</kbd> / <kbd>Mod</kbd><kbd>Tab</kbd> to switch across all windows, and <kbd>Alt</kbd><kbd>\`</kbd> / <kbd>Mod</kbd><kbd>\`</kbd> to switch between windows of the current application.
Adding <kbd>Shift</kbd> will switch windows backwards.
Adding the recent windows `binds {}` section to your config removes all default binds.
You can copy the ones you need from the summary at the top of this wiki page.
```kdl
recent-windows {
// Even an empty binds {} section will remove all default binds.
binds {
}
}
```
The available actions are `next-window` and `previous-window`.
They can optionally have the following properties:
- `filter="app-id"`: filters the switcher to the windows of the currently selected application, as determined by the Wayland app ID.
- `scope="all"`, `scope="output"`, `scope="workspace"`: sets the pre-selected scope when this bind is used to open the recent windows switcher.
```kdl
recent-windows {
// Pre-select the "Output" scope when switching windows.
binds {
Mod+Tab { next-window scope="output"; }
Mod+Shift+Tab { previous-window scope="output"; }
Mod+grave { next-window scope="output" filter="app-id"; }
Mod+Shift+grave { previous-window scope="output" filter="app-id"; }
}
}
```
The recent windows binds have lower precedence than the [normal binds](./Configuration:-Key-Bindings.md), meaning that if you have <kbd>Alt</kbd><kbd>Tab</kbd> bound to something else in the normal binds, the `recent-windows` bind won't work.
In this case, you can remove the conflicting normal bind.
All binds in this section must have a modifier key like <kbd>Alt</kbd> or <kbd>Mod</kbd> because the recent windows switcher remains open only while you hold any modifier key.
#### Bindings inside the switcher
When the switcher is open, some hardcoded binds are available:
- <kbd>Escape</kbd> cancels the switcher.
- <kbd>Enter</kbd> closes the switcher confirming the current window.
- <kbd>A</kbd>, <kbd>W</kbd>, <kbd>O</kbd> select a specific scope.
- <kbd>S</kbd> cycles between scopes, as indicated by the panel at the top.
- <kbd>←</kbd>, <kbd>→</kbd>, <kbd>Home</kbd>, <kbd>End</kbd> move the selection directionally.
Additionally, certain regular binds will automatically work in the switcher:
- focus column left/right and their variants: will move the selection left/right inside the switcher.
- focus column first/last: will move the selection to the first or last window.
- close window: will close the window currently focused in the switcher.
- screenshot: will open the screenshot UI.
The way this works is by finding all regular binds corresponding to these actions and taking just the trigger key without modifiers.
For example, if you have <kbd>Mod</kbd><kbd>Shift</kbd><kbd>C</kbd> bound to `close-window`, in the window switcher pressing <kbd>C</kbd> on its own will close the window.
This way we don't need to hardcode things like HJKL directional movements.
If you have, say, Colemak-DH MNEI binds instead, they will work for you in the window switcher (as long as they don't conflict with the hardcoded ones).
-113
View File
@@ -1,113 +0,0 @@
## General principles
These are some of the general principles that I try to follow throughout niri.
They can be sidestepped in specific circumstances if there's a good reason.
### Opening a new window should not affect the sizes of any existing windows.
This is the main annoyance with traditional tiling: you want to open a new window, but it messes with your existing window sizes.
Especially when you're looking at a big window like a browser or an image editor, want to open a quick terminal for something, and it makes the big window unusably small, or reflows the content, or clips part of the window.
The usual workaround in tiling WMs is to use more workspaces: when you need a new window, you go to an empty workspace and open it there (this way, you also get your entire screen for the new window, rather than a smaller part of it).
Scrollable tiling offers an alternative: for temporary windows, you can just open them, do what you need, and close, all without messing up the other windows or having to go to a new workspace.
It also lets you group together more related windows on the same workspace by having less frequently used ones scrolled out of the view.
### The focused window should not move around on its own.
In particular: windows opening, closing, and resizing to the left of the focused window should not cause it to visually move.
The focused window is the window you're working in.
And stuff happening outside the view shouldn't mess with what you're focused on.
### Actions should apply immediately.
This is important both for compositor responsiveness and predictability, and for keeping the code sane and free of edge cases and unnecessary asynchrony.
- Things like resizing or consuming into column take effect immediately, even if the window needs time to catch up.
- An animated workspace switch makes your input go to the final workspace and window instantly, without waiting for the animation.
- Opening the overview (which has a zoom-out animation) lets you grab windows right away, and closing the overview makes your input immediately go back to the windows, without waiting for the zoom back in.
### When disabled, eye-candy features should not affect the performance.
Things like animations and custom shaders do not run and are not present in the render tree when disabled.
Extra offscreen rendering is avoided.
Animations specifically are still "started" even when disabled, but with a duration of 0 (this way, they end as soon as the time is advanced).
This does not impact performance, but helps avoid a lot of edge cases in the code.
### Eye-candy features should not cause unreasonable excessive rendering.
- For example, clip-to-geometry will prevent direct scanout in many cases (since the window surface is not completely visible). But in the cases where the surface or the subsurface *is* completely visible (fully within the clipped region), it will still allow for direct scanout.
- For example, animations *can* cause damage and even draw to an offscreen every frame, because they are expected to be short (and can be disabled). However, something like the rounded corners shader should not offscreen or cause excessive damage every frame, because it is long-running and constantly active.
### Be mindful of invisible state.
This is niri state that is not immediately apparent from looking at the screen. This is not bad per se, but you should carefully consider how to reduce the surprise factor.
- For example, when a monitor disconnects, all its workspaces move to another connected monitor. In order to be able to restore these workspaces when the first monitor connects again, these workspaces keep the knowledge of which was their *original monitor*—this is an example of invisible state, since you can't tell it in any way by looking at the screen. This can have surprising consequences: imagine disconnecting a monitor at home, going to work, completely rearranging the windows there, then coming back home, and suddenly some random workspaces end up on your home monitor. In order to reduce this surprise factor, whenever a new window appears on a workspace, that workspace resets its *original monitor* to its current monitor. This way, the workspaces you actively worked on remain where they were.
- For example, niri preserves the view position whenever a window appears, or whenever a window goes full-screen, to restore it afterward. This way, dealing with temporary things like dialogs opening and closing, or toggling full-screen, becomes less annoying, since it doesn't mess up the view position. This is also invisible state, as you cannot tell by looking at the screen where closing a window will restore the view position. If taken to the extreme (previous view position saved forever for every open window), this can be surprising, as closing long-running windows would result in the view shifting around pretty much randomly. To reduce this surprise factor, niri remembers only one last view position per workspace, and forgets this stored view position upon window focus change.
## Window layout
Here are some design considerations for the window layout logic.
1. If a window or popup is larger than the screen, it should be aligned in the top left corner.
The top left area of a window is more likely to contain something important, so it should always be visible.
1. Setting window width or height to a fixed pixel size (e.g. `set-column-width 1280` or `default-column-width { fixed 1280; }`) will set the size of the window itself, however setting to a proportional size (e.g. `set-column-width 50%`) will set the size of the tile, including the border added by niri.
- With proportions, the user is looking to tile multiple windows on the screen, so they should include borders.
- With fixed sizes, the user wants to test a specific client size or take a specifically sized screenshot, so they should affect the window directly.
- After the size is set, it is always converted to a value that includes the borders, to make the code sane. That is, `set-column-width 1000` followed by changing the niri border width will resize the window accordingly.
1. Fullscreen windows are a normal part of the scrolling layout.
This is a cool idea that scrollable tiling is uniquely positioned to implement.
Fullscreen windows aren't on some "special" layer that covers everything; instead, they are normal tiles that you can switch away from, without disturbing the fullscreen status.
Of course, you do want to cover your entire monitor when focused on a fullscreen window.
This is specifically hardcoded into the logic: when the view is stationary on a focused fullscreen window, the top layer-shell layer and the floating windows hide away.
This is also why fullscreening a floating window makes it go into the scrolling layout.
## Default config
The [default config](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
Importantly, it is not a "suggested rice config"; we don't want to startle people with full-on rainbow borders and crazy shaders.
Since we're not a complete desktop environment (and don't have the contributor base to become one), we cannot provide a fully integrated experience—distro spins are better positioned to do this.
As such, new niri users are expected to read through and tinker with the default niri config.
The default config is therefore thoroughly commented with links to the relevant wiki sections.
We don't include every possible option in the default config to avoid overwhelming users too much; anything overly specific or uncommon can stay on the wiki.
The general rule is to include things that users are reasonably expected to want to change or know how to do.
We do also advertise our more unique features though like screencast block-out-from.
We default to CSD (`prefer-no-csd` is commented out).
This gives new users easy and familiar way to move and close windows via their titlebars, especially considering that niri doesn't have serverside titlebars (so far at least).
Focus rings are drawn fully behind windows by default.
While this unfortunately messes with window transparency, [which is a common source of confusion](./FAQ.md#why-are-transparent-windows-tinted-why-is-the-borderfocus-ring-showing-up-through-semitransparent-windows), defaulting to drawing focus rings only around windows would be even worse because it has holes inside clientside rounded corners.
The ideal solution here would be to propose a Wayland protocol for windows to report their corner radius to the compositor (which would generally help for serverside decorations in different compositors).
The default focus ring is quite thick at 4 px to look well with clientside-decorated windows and be obviously noticeable, and the default gaps are also quite big at 16 px to look well with the default focus ring width.
The default input settings like touchpad tap and natural-scroll are how I believe most people want to use their computers.
Shadows default to off because they are a fairly performance-intensive shader, and because many clientside-decorated windows already draw their own shadows.
The default screenshot-path matches GNOME Shell.
Default window rules are limited to fixing known severe issues (WezTerm) and doing something the absolute majority likely wants (make Firefox Picture-in-Picture player floating—it can't do that on its own currently, maybe the pip protocol will change that).
The default binds largely come from my own experience using PaperWM, and from other compositors.
They assume QWERTY.
The binds are ordered in a way to gradually introduce you to different bind configuration concepts.
The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kbd> will move the focused window or column there.
Adding <kbd>Shift</kbd> does an alternative action: for focus and movement it starts going across monitors, for resizes it starts acting on window height rather than width, etc.
Workspace switching on <kbd>Mod</kbd><kbd>U</kbd>/<kbd>I</kbd> is one key up from <kbd>Mod</kbd><kbd>J</kbd>/<kbd>K</kbd> used for window switching.
Since <kbd>Alt</kbd> is a modifier in nested niri, binds with explicit <kbd>Alt</kbd> are mainly the ones only useful on the host, for example spawning a screen locker.
@@ -1,85 +0,0 @@
niri's documentation files are found in `docs/wiki/` and should be viewable and browsable in at least three systems:
- The GitHub repo's markdown file preview
- [The GitHub repo's wiki](https://github.com/niri-wm/niri/wiki)
- [The documentation site](https://niri-wm.github.io/niri/)
## The GitHub repo's wiki
This is generated with the `publish-wiki` job in `.github/workflows/ci.yml`.
In order to have this job run as expected in your fork, you'll need to enable the wiki feature in your repo's settings on GitHub.
This could be useful as a contributor to verify that the wiki generates the way you expect it to.
## The documentation site
The documentation site is generated with [mkdocs](https://www.mkdocs.org/).
The configuration files are found in `docs/`.
To set up and run the documentation site locally, it is recommended to use [uv](https://docs.astral.sh/uv/).
### Serving the site locally with uv
In the `docs/` subdirectory:
- `uv sync`
- `uv run mkdocs serve`
The documentation site should now be available on http://127.0.0.1:8000/niri/
Changes made to the documentation while the development server is running will cause an automatic page refresh in the browser.
> [!TIP]
> Images may not be visible, as they are stored on Git LFS.
> If this is the case, run `git lfs pull`.
## Elements
Elements such as links, admonitions, images, and snippets should work as expected in markdown file previews on GitHub, the GitHub repo's wiki, and in the documentation site.
### Links
Links should in all cases be relative (e.g. `./FAQ.md`), unless it's an external one.
Links should have anchors if they are meant to lead the user to a specific section on a page (e.g. `./Getting-Started.md#nvidia`).
> [!TIP]
> mkdocs will terminate if relative links lead to non-existing documents or non-existing anchors.
> This means that the CI pipeline will fail when building documentation, as will `mkdocs serve` locally.
### Admonitions
> [!IMPORTANT]
> This is an important distinction from other `mkdocs`-based documentation you might have encountered.
Admonitions, or alerts should be written [the way GitHub defines them](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts).
The above admonition is written like this:
```
> [!IMPORTANT]
> This is an important distinction from other `mkdocs`-based documentation you might have encountered.
```
### Images
Images should have relative links to resources in `docs/wiki/img/`, and should contain sensible alt-text.
### Videos
For compatibility with both mkdocs and GitHub Wiki, videos need to be wrapped in a `<video>` tag (displayed by mkdocs) and have the video link again as fallback text (displayed by GitHub Wiki) padded with blank lines.
```html
<video controls src="https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995">
https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995
</video>
```
### Snippets
Configuration and code snippets in general should be annotated with a language.
If the language used in the snippet is KDL, open the code block like this:
```md
```kdl
```
-161
View File
@@ -1,161 +0,0 @@
### How to disable client-side decorations/make windows rectangular?
Uncomment the [`prefer-no-csd` setting](./Configuration:-Miscellaneous.md#prefer-no-csd) at the top level of the config, and then restart your apps.
Then niri will ask windows to omit client-side decorations, and also inform them that they are being tiled (which makes some windows rectangular, even if they cannot omit the decorations).
Note that currently this will prevent edge window resize handles from showing up.
You can still resize windows by holding <kbd>Mod</kbd> and the right mouse button.
### Why are transparent windows tinted? / Why is the border/focus ring showing up through semitransparent windows?
Uncomment the [`prefer-no-csd` setting](./Configuration:-Miscellaneous.md#prefer-no-csd) at the top level of the config, and then restart your apps.
Niri will draw focus rings and borders *around* windows that agree to omit their client-side decorations.
By default, focus ring and border are rendered as a solid background rectangle behind windows.
That is, they will show up through semitransparent windows.
This is because windows using client-side decorations can have an arbitrary shape.
You can also override this behavior with the [`draw-border-with-background` window rule](./Configuration:-Window-Rules.md#draw-border-with-background).
### How to enable rounded corners for all windows?
Put this window rule in your config:
```kdl
window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
```
For more information, check the [`geometry-corner-radius` window rule](./Configuration:-Window-Rules.md#geometry-corner-radius).
### How to hide the "Important Hotkeys" pop-up at the start?
Put this into your config:
```kdl
hotkey-overlay {
skip-at-startup
}
```
### How to fix lag on external monitors connected to a hybrid GPU laptop?
Hybrid GPU laptops (which have both an integrated and a discrete GPU) generally connect the external monitor port to the discrete GPU.
Meanwhile, the built-in monitor is connected to the integrated GPU, and the integrated GPU is used for rendering by default.
This is good and expected because the integrated GPU uses significantly less battery compared to the discrete GPU.
However, this means that niri has to render the external monitor contents on the integrated GPU, then copy them over to the discrete GPU for display.
On some laptops this can cause lag and stuttering (it gets worse with monitor resolution and refresh rate).
If your laptop has a MUX switch—usually a GPU toggle in the UEFI settings—then you can switch it to use the discrete GPU, then niri will render on the discrete GPU, and the external monitor won't lag.
Otherwise, you can try configuring niri to render on the discrete GPU via the [`render-drm-device`](./Configuration:-Debug-Options.md#render-drm-device) debug option.
Keep in mind that using the discrete GPU for rendering will make the laptop's battery deplete much faster.
### How to run X11 apps like Steam or Discord?
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
Check [the Xwayland wiki page](./Xwayland.md) for instructions.
Keep in mind that you can run many Electron apps such as VSCode or Discord natively on Wayland by passing the right flags, as described [here](./Application-Issues.md#electron-applications).
### Why doesn't niri integrate Xwayland like other compositors?
A combination of factors:
- Integrating Xwayland is quite a bit of work, as the compositor needs to implement parts of an X11 window manager.
- You need to appease the X11 ideas of windowing, whereas for niri I want to have the best code for Wayland.
- niri doesn't have a good global coordinate system required by X11.
- You tend to get an endless stream of X11 bugs that take further time and effort away from other tasks.
- There aren't actually that many X11-only clients nowadays, and xwayland-satellite takes perfect care of most of those.
- niri isn't a Big Serious Desktop Environment which Must Support All Use Cases (and is Backed By Some Corporation).
All in all, the situation works out in favor of avoiding Xwayland integration.
<sup>Since: 25.08</sup> niri has seamless built-in xwayland-satellite integration that by and large works as well as built-in Xwayland in other compositors, solving the hurdle of having to set it up manually.
I wouldn't be too surprised if, down the road, xwayland-satellite becomes the standard way of integrating Xwayland into new compositors, since it takes on the bulk of the annoying work, and isolates the compositor from misbehaving clients.
### Can I enable blur behind semitransparent windows?
<sup>Since: 26.04</sup> Yes.
See the [window effects](./Window-Effects.md) wiki page.
### Can I make a window sticky / pinned / always on top / appear on all workspaces?
Not yet, follow/upvote [this issue](https://github.com/niri-wm/niri/issues/932).
You can emulate this with a script that uses the niri IPC.
For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature (`toggle-follow-mode`).
### How do I make the Bitwarden window in Firefox open as floating?
Firefox seems to first open the Bitwarden window with a generic Firefox title, and only later change the window title to Bitwarden, so you can't effectively target it with an `open-floating` window rule.
You'll need to use a script, for example [this one](https://github.com/niri-wm/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
### Can I open a window directly in the current column / in the same column as another window?
No, but you can script the behavior you want with the [niri IPC](./IPC.md).
Listen to the event stream for a new window opening, then call an action like `consume-or-expel-window-left`.
Adding this directly to niri is challenging:
- The act of "opening a window directly in some column" by itself is quite involved. Niri will have to compute the exact initial window size provided how other windows in a column would resize in response. This logic exists, but it isn't directly pluggable to the code computing a size for a new window. Then, it'll need to handle all sorts of edge cases like the column disappearing, or new windows getting added to the column, before the target window had a chance to appear.
- How do you indicate if a new window should spawn in an existing column (and in which one), as opposed to a new column? Different people seem to have different needs here (including very complex rules based on parent PID, etc.), and it's very unclear design-wise what kind of (simple) setting is actually needed and would be useful. See also https://github.com/niri-wm/niri/discussions/1125.
### Why does moving the mouse against a monitor edge focus the next window, but only sometimes?
This can happen with [`focus-follows-mouse`](./Configuration:-Input.md#focus-follows-mouse).
When using client-side decorations, windows are supposed to have some margins outside their geometry for the mouse resizing handles.
These margins "peek out" of the monitor edges since they're outside the window geometry, and `focus-follows-mouse` triggers when the mouse crosses them.
It doesn't always happen:
- Some toolkits don't put resize handles outside the window geometry. Then there's no input area outside, so nowhere for `focus-follows-mouse` to trigger.
- If the current window has its own margin for resizing, and it extends all the way to the monitor edge, then `focus-follows-mouse` won't trigger because the mouse will never leave the current window.
To fix this, you can:
- Use `focus-follows-mouse max-scroll-amount="0%"`, which will prevent `focus-follows-mouse` from triggering when it would cause scrolling.
- Set `prefer-no-csd` which will generally cause clients to remove those resizing margins.
### How do I recover from a dead screen locker / from a red screen?
When your screen locker dies, you will be left with a red screen.
This is niri's locked session background.
You can recover from this by spawning a new screen locker.
One way is to switch to a different TTY (with a shortcut like <kbd>Ctrl</kbd><kbd>Alt</kbd><kbd>F3</kbd>) and spawning a screen locker to niri's Wayland display, e.g. `WAYLAND_DISPLAY=wayland-1 swaylock`.
Another way is to set `allow-when-locked=true` on your screen locker bind, then you can press it on the red screen to get a fresh screen locker.
```kdl
binds {
Super+Alt+L allow-when-locked=true { spawn "swaylock"; }
}
```
### How do I change output configuration based on connected monitors?
If you require different output configurations depending on what outputs are connected then you can use [Kanshi](https://gitlab.freedesktop.org/emersion/kanshi).
Kanshi has its own simple configuration and communicates with niri via IPC. You may want to launch kanshi from the niri config.kdl e.g. `spawn-at-startup "/usr/bin/kanshi"`
For example, if you wish to scale your laptop display differently when an external monitor is connected, you might use a Kanshi config like this:
```
profile {
output eDP-1 enable scale 1.0
}
profile {
output HDMI-A-1 enable scale 1.0 position 0,0
output eDP-1 enable scale 1.25 position 1920,0
}
```
### Why does Firefox or Thunderbird have 1 px smaller border?
They draw their own 1 px dark border around the window, which obscures one pixel of niri's border.
If you don't like this, set the [`clip-to-geometry true` window rule](./Configuration:-Window-Rules.md#clip-to-geometry).
-81
View File
@@ -1,81 +0,0 @@
There are several ways to make a window big on niri: maximizing the column, maximizing the window to edges, and fullscreening the window.
Let's look at their differences.
## Maximized (full-width) columns
Maximizing the column via `maximize-column` (bound to <kbd>Mod</kbd><kbd>F</kbd> by default) expands its width to cover the whole screen.
Maximized columns still leave space for [struts] and [gaps], and can contain multiple windows.
The windows retain their borders.
This is the simplest of the sizing modes, and is equivalent to `proportion 1.0` column width, or `set-column-width "100%"`.
![Screenshot of a maximized column with two windows.](./img/maximized-column.png)
You can make a window open in a maximized column with the [`open-maximized true`](./Configuration:-Window-Rules.md#open-maximized) window rule.
## Windows maximized to edges
<sup>Since: 25.11</sup>
You can maximize an individual window via `maximize-window-to-edges` (bound to <kbd>Mod</kbd><kbd>M</kbd> by default).
This is the same maximize as you can find on other desktop environments and operating systems: it expands a window to the edges of the available screen area.
You will still see your bar, but not struts, gaps, or borders.
Windows are aware of their maximized-to-edges status and generally respond by squaring their corners.
Windows can also control maximizing-to-edges: when you click on the square icon in the window's titlebar, or double-click on the titlebar, the window will request niri to maximize or unmaximize itself.
You can put multiple maximized windows into a [tabbed column](./Tabs.md), but not into a regular column.
![Screenshot of a window maximized to edges.](./img/window-maximized-to-edges.png)
You can make a window open maximized-to-edges, or prevent a window from maximizing upon opening, with the [`open-maximized-to-edges`](./Configuration:-Window-Rules.md#open-maximized-to-edges) window rule.
## Fullscreen windows
Windows can go fullscreen, usually seen with video players, presentations or games.
You can also force a window to go fullscreen via `fullscreen-window` (bound to <kbd>Mod</kbd><kbd>Shift</kbd><kbd>F</kbd> by default).
Fullscreen windows cover the entire screen.
Similarly to maximize-to-edges, windows are aware of their fullscreen status, and can respond by hiding their titlebars or other parts of the UI.
Niri renders a solid black backdrop behind fullscreen windows.
This backdrop helps match the screen size when the window itself remains too small (e.g. if you try to fullscreen a fixed-size dialog window), which is the behavior [defined by the Wayland protocol](https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_fullscreen).
When a fullscreen window is focused and not animating, it will cover floating windows and the top layer-shell layer.
If you want for example your layer-shell notifications or launcher to appear over fullscreen windows, configure the respective tools to put them on the overlay layer-shell layer.
![Screenshot of a fullscreen window.](./img/fullscreen-window.png)
You can make a window open fullscreen, or prevent a window from fullscreening upon opening, with the [`open-fullscreen`](./Configuration:-Window-Rules.md#open-fullscreen) window rule.
## Common behaviors across fullscreen and maximize
Fullscreen or maximized-to-edges windows can only be in the scrolling layout.
So if you try to fullscreen or maximize a [floating window](./Floating-Windows.md), it'll move into the scrolling layout.
Then, unfullscreening/unmaximizing will bring it back into the floating layout automatically.
Thanks to scrollable tiling, fullscreen and maximized windows remain a normal participant of the layout: you can scroll left and right from them and see other windows.
![Screenshot of the overview showing a fullscreen window with other windows side by side.](./img/fullscreen-window-in-overview.png)
Fullscreen and maximize-to-edges are both special states that the windows are aware of and can control.
Windows sometimes want to restore their fullscreen or, more frequently, maximized state when they open.
The best opportunity for this is during the *initial configure* sequence when the window tells niri everything it should know before opening the window.
If the window does this, then `open-maximized-to-edges` and `open-fullscreen` window rules have a chance to block or adjust the request.
However, some clients tend to request to be maximized shortly *after* the initial configure sequence, when the niri already sent them the initial size request (sometimes even after showing on screen, resulting in a quick resize right after opening).
From niri's point of view, the window is already open by this point, so if the window does this, then the `open-maximized-to-edges` and `open-fullscreen` window rules don't do anything.
## Windowed fullscreen
<sup>Since: 25.05</sup>
Niri can also tell a window that it's in fullscreen without actually making it fullscreen, via the `toggle-windowed-fullscreen` action.
This is generally useful for screencasting browser-based presentations, when you want to hide the browser UI, but still have the window sized as a normal window.
When in windowed fullscreen, you can use the niri action to maximize or unmaximize the window.
Window-side titlebar maximize buttons and gestures may not work, since the window will always think that it's in fullscreen.
See also windowed fullscreen on the [screencasting features wiki page](./Screencasting.md#windowed-fakedetached-fullscreen).
[struts]: ./Configuration:-Layout.md#struts
[gaps]: ./Configuration:-Layout.md#gaps
-65
View File
@@ -1,65 +0,0 @@
This page contains various bits of information helpful for integrating niri in a distribution.
First, for creating a niri package, see the [Packaging](./Packaging-niri.md) page.
### Configuration
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
This means that you can customize your distribution defaults by creating `/etc/niri/config.kdl`.
When this file is present, niri *will not* automatically create a config at `~/.config/niri/`, so you'll need to direct your users how to do it themselves.
Keep in mind that we update the default config in new releases, so if you have a custom `/etc/niri/config.kdl`, you likely want to inspect and apply the relevant changes too.
The default configuration locations can be overridden with the `NIRI_CONFIG` environment variable.
<sup>Since: 26.04</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
<sup>Since: 25.11</sup> You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
### Xwayland
Xwayland is required for running X11 apps and games, and also the Orca screen reader.
<sup>Since: 25.08</sup> Niri integrates with [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) out of the box.
The integration requires xwayland-satellite >= 0.7 available in `$PATH`.
Please consider making niri depend on (or at least recommend) the xwayland-satellite package.
If you had a custom config which manually started `xwayland-satellite` and set `$DISPLAY`, you should remove those customizations for the automatic integration to work.
You can change the path where niri looks for xwayland-satellite using the [`xwayland-satellite` top-level option](./Configuration:-Miscellaneous.md#xwayland-satellite).
### Keyboard layout
<sup>Since: 25.08</sup> By default (unless [manually configured](./Configuration:-Input.md#layout) otherwise), niri reads keyboard layout settings from systemd-localed at `org.freedesktop.locale1` over D-Bus.
Make sure your system installer sets the keyboard layout via systemd-localed, and niri should pick it up.
### Autostart
Niri works with the normal systemd autostart.
The default [niri.service](https://github.com/niri-wm/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
To make a program run at niri startup without editing the niri config, you can either link its .desktop to `~/.config/autostart/`, or use a .service file with `WantedBy=graphical-session.target`.
See the [example systemd setup](./Example-systemd-Setup.md) page for some examples.
If this is inconvenient, you can also add [`spawn-at-startup`](./Configuration:-Miscellaneous.md#spawn-at-startup) lines in the niri config.
### Screen readers
<sup>Since: 25.08</sup> Niri works with the [Orca](https://orca.gnome.org) screen reader.
Please see the [Accessibility](./Accessibility.md) page for details and advice for accessibility-focused distributions.
### Desktop components
You very likely want to run at least a notification daemon, portals, and an authentication agent.
This is detailed on the [Important Software](./Important-Software.md) page.
On top of that, you may want to preconfigure some desktop shell components to make the experience less barebones.
Niri's default config spawns [Waybar](https://github.com/Alexays/Waybar), which is a good starting point, but you may want to consider changing its default configuration to be less of a kitchen sink, and adding the `niri/workspaces` module.
You will probably also want a desktop background tool ([swaybg](https://github.com/swaywm/swaybg) or [awww (which used to be swww)](https://codeberg.org/LGFae/awww/)), and a nicer screen locker (compared to the default `swaylock`), like [hyprlock](https://github.com/hyprwm/hyprlock/).
Alternatively, some desktop environments and shells work with niri, and can give a more cohesive experience in one package:
- [LXQt](https://lxqt-project.org/) officially supports niri, see [their wiki](https://lxqt-project.org/wiki/Wayland-Session) for details on setting it up.
- Many [XFCE](https://www.xfce.org/) components work on Wayland, including niri. See [their wiki](https://wiki.xfce.org/releng/wayland_roadmap#component_specific_status) for details.
- There are complete desktop shells based on Quickshell that support niri, for example [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell) and [Noctalia](https://github.com/noctalia-dev/noctalia-shell).
- You can run a [COSMIC](https://system76.com/cosmic/) session with niri using [cosmic-ext-extra-sessions](https://github.com/Drakulix/cosmic-ext-extra-sessions).
-5
View File
@@ -1,5 +0,0 @@
Things to keep in mind with layer-shell components (bars, launchers, etc.):
1. When a [full-screen](./Fullscreen-and-Maximize.md) window is active and covers the entire screen, it will render above the top layer, and it will be prioritized for keyboard focus. If your launcher uses the top layer, and you try to run it while looking at a full-screen window, it won't show up. Only the overlay layer will show up on top of full-screen windows.
1. Components on the bottom and background layers will receive *on-demand* keyboard focus as expected. However, they will only receive *exclusive* keyboard focus when there are no windows on the workspace.
1. When opening the [Overview](./Overview.md), components on the bottom and background layers will zoom out and remain on the workspaces, while the top and overlay layers remain on top of the Overview. So, if you want the bar to remain on top, put it on the *top* layer.
-16
View File
@@ -1,16 +0,0 @@
The name "niri" is canonically written in lower-case, but feel free to capitalize it if you'd like, especially at the start of sentences where the grammatical rules require it.
This name is not intended to mean or stand for anything.
Our logo comes in four versions: full-sized, simple full-sized, icon, and simple icon.
The simple versions are single-color and suitable for smaller sizes.
| | full-sized | icon |
|--------|:------------------------------:|:------------------------------:|
| normal | ![](./logo/niri-logo.svg) | ![](./logo/niri-icon.svg) |
| simple | ![](./logo/niri-logo-smol.svg) | ![](./logo/niri-icon-smol.svg) |
The logo is intentionally recolorable.
In fact, there's [a webpage](https://nirilogo.raurutuchr.ink) that lets you quickly adjust the color and download an SVG.
All versions of the logo are licensed under [CC BY-SA](https://creativecommons.org/licenses/by-sa/4.0/).
The full-sized logo is based on the [Cherry Bomb One](https://github.com/satsuyako/CherryBomb) font, licensed under the [SIL Open Font License 1.1](https://openfontlicense.org/).
-55
View File
@@ -1,55 +0,0 @@
### High VRAM usage fix
Presently, there is a quirk in the NVIDIA drivers that affects niri's VRAM usage (the driver does not properly release VRAM back into the pool). Niri *should* use on the order of 100 MiB of VRAM (as checked in [nvtop](https://github.com/Syllo/nvtop)); if you see anywhere close to 1 GiB of VRAM in use, you are likely hitting this issue (heap not returning freed buffers to the driver).
Luckily, you can mitigate this by configuring the NVIDIA drivers with a per-process application profile as follows:
* `sudo mkdir -p /etc/nvidia/nvidia-application-profiles-rc.d` to make the config dir if it does not exist (it most likely does not if you are reading this)
* write the following JSON blob to set the `GLVidHeapReuseRatio` config value for the `niri` process into the file `/etc/nvidia/nvidia-application-profiles-rc.d/50-limit-free-buffer-pool-in-wayland-compositors.json`:
```json
{
"rules": [
{
"pattern": {
"feature": "procname",
"matches": "niri"
},
"profile": "Limit Free Buffer Pool On Wayland Compositors"
}
],
"profiles": [
{
"name": "Limit Free Buffer Pool On Wayland Compositors",
"settings": [
{
"key": "GLVidHeapReuseRatio",
"value": 0
}
]
}
]
}
```
(The file in `/etc/nvidia/nvidia-application-profiles-rc.d/` can be named anything, and does not actually need an extension).
Restart niri after writing the config file to apply the change.
The upstream issue that this solution was pulled from is [here](https://github.com/NVIDIA/egl-wayland/issues/126#issuecomment-2379945259). There is a (slim) chance that NVIDIA updates their built-in application profiles to apply this to niri automatically; it is unlikely that the underlying heuristic will see a proper fix.
The fix shipped in the driver at the time of writing uses a value of 0, while the initial config posted by an Nvidia engineer approximately a year prior used a value of 1.
### Screencast flickering fix
<sup>Until: 25.08</sup>
If you have screencast glitches or flickering on NVIDIA, set this in the niri config:
```kdl,must-fail
debug {
wait-for-frame-completion-in-pipewire
}
```
This debug flag has since been removed because the problem was properly fixed in niri.
-116
View File
@@ -1,116 +0,0 @@
### Overview
<sup>Since: 25.05</sup>
The Overview is a zoomed-out view of your workspaces and windows.
It lets you see what's going on at a glance, navigate, and drag windows around.
<video controls src="https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995">
https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995
</video>
Open it with the `toggle-overview` bind, via the top-left hot corner, or using a touchpad four-finger swipe up.
While in the overview, all keyboard shortcuts keep working, while pointing devices get easier:
- Mouse: left click and drag windows to move them, right click and drag to scroll workspaces left/right, scroll to switch workspaces (no holding Mod required).
- Touchpad: two-finger scrolling that matches the normal three-finger gestures.
- Touchscreen: one-finger scrolling, or one-finger long press to move a window.
> [!TIP]
> The overview needs to draw a background under every workspace.
> So, layer-shell surfaces work this way: the *background* and *bottom* layers zoom out together with the workspaces, while the *top* and *overlay* layers remain on top of the overview.
>
> Put your bar on the *top* layer.
Drag-and-drop will scroll the workspaces up/down in the overview, and will activate a workspace when holding it for a moment.
Combined with the hot corner, this lets you do a mouse-only DnD across workspaces.
<video controls src="https://github.com/user-attachments/assets/5f09c5b7-ff40-462b-8b9c-f1b8073a2cbb">
https://github.com/user-attachments/assets/5f09c5b7-ff40-462b-8b9c-f1b8073a2cbb
</video>
You can also drag-and-drop a window to a new workspace above, below, or between existing workspaces.
<video controls src="https://github.com/user-attachments/assets/b76d5349-aa20-4889-ab90-0a51554c789d">
https://github.com/user-attachments/assets/b76d5349-aa20-4889-ab90-0a51554c789d
</video>
### Configuration
See the full documentation for the `overview {}` section [here](./Configuration:-Miscellaneous.md#overview).
You can set the zoom-out level like this:
```kdl
// Make workspaces four times smaller than normal in the overview.
overview {
zoom 0.25
}
```
To change the color behind the workspaces, use the `backdrop-color` setting:
```kdl
// Make the backdrop light.
overview {
backdrop-color "#777777"
}
```
You can also disable the hot corner:
```kdl
// Disable the hot corners.
gestures {
hot-corners {
off
}
}
```
### Backdrop customization
Apart from setting a custom backdrop color like described above, you can also put a layer-shell wallpaper into the backdrop with a [layer rule](./Configuration:-Layer-Rules.md#place-within-backdrop), for example:
```kdl
// Put swaybg inside the overview backdrop.
layer-rule {
match namespace="^wallpaper$"
place-within-backdrop true
}
```
This will only work for *background* layer surfaces that ignore exclusive zones (typical for wallpaper tools).
You can run two different wallpaper tools (like swaybg and awww), one for the backdrop and one for the normal workspace background.
This way you could set the backdrop one to a blurred version of the wallpaper for a nice effect.
You can also combine this with a transparent background color if you don't like the wallpaper moving together with workspaces:
```kdl
// Make the wallpaper stationary, rather than moving with workspaces.
layer-rule {
// This is for swaybg; change for other wallpaper tools.
// Find the right namespace by running niri msg layers.
match namespace="^wallpaper$"
place-within-backdrop true
}
// Set transparent workspace background color.
layout {
background-color "transparent"
}
// Optionally, disable the workspace shadows in the overview.
overview {
workspace-shadow {
off
}
}
```
-15
View File
@@ -1,15 +0,0 @@
Welcome to the niri documentation!
Feel free to look through usage and [Getting started](./Getting-Started.md).
If you're looking for ways to configure niri, check out the [introduction to configuration](./Configuration:-Introduction.md).
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
If you're not already here, check out our new wiki website! https://niri-wm.github.io/niri/
---
The documentation is open to contribution, see [Documenting niri](./Development:-Documenting-niri.md).
Please discuss bigger changes in [our Matrix room](https://matrix.to/#/#niri:matrix.org) first!
The wiki is generated from files in the `docs/wiki/` folder of the repository, so you can open a pull request modifying it there.
-85
View File
@@ -1,85 +0,0 @@
### Overview
<sup>Since: 26.04</sup>
You can apply background effects to windows and layer-shell surfaces.
These include blur, xray, saturation, and noise.
They can be enabled in the `background-effect {}` section of [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rules.
The window needs to be semitransparent for you to see the background effect (otherwise it's fully covered by the opaque window).
Focus ring and border can also cover the background effect, see [this FAQ entry](./FAQ.md#why-are-transparent-windows-tinted-why-is-the-borderfocus-ring-showing-up-through-semitransparent-windows) for how to change this.
### Blur
Windows and layer surfaces can request their background to be blurred via the [`ext-background-effect` protocol](https://wayland.app/protocols/ext-background-effect-v1).
In this case, the application will usually offer some "background blur" setting that you'll need to enable in its configuration.
You can also enable blur on the niri side with the `blur true` background effect window rule:
```kdl
// Enable blur behind the Alacritty terminal.
window-rule {
match app-id="^Alacritty$"
background-effect {
blur true
}
}
// Enable blur behind the fuzzel launcher.
layer-rule {
match namespace="^launcher$"
background-effect {
blur true
}
}
```
Blur enabled via the window rule will follow the window corner radius set via [`geometry-corner-radius`](./Configuration:-Window-Rules.md#geometry-corner-radius).
On the other hand, blur enabled through `ext-background-effect` will exactly follow the shape requested by the window.
If the window or layer has clientside rounded corners or other complex shape, it should set a corresponding blur shape through `ext-background-effect`, then it will get correctly shaped background blur without any manual niri configuration.
Windows can also blur their pop-up menus using `ext-background-effect`.
On the niri side, you can do it with a `popups` block inside [`window-rule`](./Configuration:-Window-Rules.md#popups) and [`layer-rule`](./Configuration:-Layer-Rules.md#popups).
See those wiki pages for examples and limitations.
Global blur settings are configured in the [`blur {}` config section](./Configuration:-Miscellaneous.md#blur) and apply to all background blur.
### Xray
Xray makes the window background "see through" to your wallpaper, ignoring any other windows below.
You can enable it with `xray true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
Xray is automatically enabled by default if any other background effect (like blur) is active.
This is because it's much more efficient: with xray active, niri only needs to blur the background once, and then can reuse this blurred version with no extra work (since the wallpaper changes very rarely).
If you have an animated wallpaper, xray will still have to recompute blur every frame, but that happens once and shared among all windows, rather than recomputed separately for each window.
#### Non-xray effects (experimental)
You can disable xray with `xray false` background effect window rule.
This gives you the normal kind of blur where everything below a window is blurred.
Keep in mind that non-xray blur and other non-xray effects are more expensive as niri has to recompute them any time you move the window, or the contents underneath change.
> [!WARNING]
> Non-xray effects are currently experimental because they have some known limitations.
>
> - They disappear during window open/close animations and while dragging a tiled window.
> Fixing this requires a refactor to the niri rendering code to defer offscreen rendering, and possibly other refactors.
### Implementation notes
The `ext-background-effect` protocol supports any wl_surface.
We currently implement it only for toplevels, layer surfaces, and pop-ups, which should cover the vast majority of what's actually used by applications.
For pop-ups, effects default to *non-xray* because pop-ups generally appear on top of windows.
In particular, the following surface types don't support `ext-background-effect`.
They can be implemented as the need arises.
- Subsurfaces. Would require implementing `clip-to-geometry` support for background effects.
- Lock surfaces. Not useful as it would just show our red locked session background.
- Cursor and drag-and-drop icon.
The main challenge here will be screencasts where the cursor is rendered separately.
This is problematic because non-xray effects require rendering the whole scene in one go rather than separately.
-405
View File
@@ -1,405 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512.00006"
viewBox="0 0 135.46667 135.46668"
version="1.1"
id="svg1"
sodipodi:docname="icon.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:zoom="0.8030909"
inkscape:cx="26.14897"
inkscape:cy="535.43129"
inkscape:window-width="2528"
inkscape:window-height="1408"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="g71" />
<defs
id="defs1">
<filter
style="color-interpolation-filters:sRGB"
id="filter21"
x="-0.35129357"
y="-0.93287114"
width="1.7025871"
height="2.8657423">
<feGaussianBlur
stdDeviation="5.0792937"
id="feGaussianBlur21" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter22"
x="-0.42891072"
y="-0.64938287"
width="1.8578214"
height="2.2987657">
<feGaussianBlur
stdDeviation="6.5904956"
id="feGaussianBlur22" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter23"
x="-0.41511482"
y="-0.64325018"
width="1.8302296"
height="2.2865004">
<feGaussianBlur
stdDeviation="7.8584288"
id="feGaussianBlur23" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter24"
x="-0.36140346"
y="-0.92623631"
width="1.7228069"
height="2.8524726">
<feGaussianBlur
stdDeviation="5.269127"
id="feGaussianBlur24" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter27"
x="-0.17576354"
y="-0.83601771"
width="1.3515271"
height="2.6720354">
<feGaussianBlur
stdDeviation="4.753432"
id="feGaussianBlur26" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter52"
x="-0.33025388"
y="-1.0968854"
width="1.6605078"
height="3.1937708">
<feGaussianBlur
stdDeviation="4.2575558"
id="feGaussianBlur52" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter53"
x="-0.34014132"
y="-1.0243618"
width="1.6802826"
height="3.0487236">
<feGaussianBlur
stdDeviation="4.3187927"
id="feGaussianBlur54" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter55"
x="-0.33171227"
y="-1.0929411"
width="1.6634245"
height="3.1858823">
<feGaussianBlur
stdDeviation="4.4263355"
id="feGaussianBlur55" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter56"
x="-0.34596671"
y="-0.98818076"
width="1.6919334"
height="2.9763615">
<feGaussianBlur
stdDeviation="4.9267071"
id="feGaussianBlur56" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter57"
x="-0.50604737"
y="-0.53225252"
width="2.0120947"
height="2.064505">
<feGaussianBlur
stdDeviation="5.8261111"
id="feGaussianBlur57" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter58"
x="-0.32130078"
y="-1.1307663"
width="1.6426016"
height="3.2615325">
<feGaussianBlur
stdDeviation="4.2390405"
id="feGaussianBlur58" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter59"
x="-0.4030115"
y="-0.71156066"
width="1.806023"
height="2.4231213">
<feGaussianBlur
stdDeviation="4.4950086"
id="feGaussianBlur59" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter60"
x="-0.1759294"
y="-1.0177407"
width="1.3518588"
height="3.0354814">
<feGaussianBlur
stdDeviation="4.8003951"
id="feGaussianBlur60" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter70"
x="-0.43159762"
y="-0.3122953"
width="1.8631952"
height="1.6245906">
<feGaussianBlur
stdDeviation="18.461288"
id="feGaussianBlur70" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter72"
x="-0.43271319"
y="-0.31310251"
width="1.8654264"
height="1.626205">
<feGaussianBlur
stdDeviation="18.509006"
id="feGaussianBlur72" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter1"
x="-0.45705071"
y="-0.57220236"
width="1.9141014"
height="2.1444047">
<feGaussianBlur
stdDeviation="8.5810337"
id="feGaussianBlur1" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter2"
x="-0.545201"
y="-0.88686217"
width="2.090402"
height="2.7737243">
<feGaussianBlur
stdDeviation="4.3199889"
id="feGaussianBlur2" />
</filter>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path5" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath5">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path8" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath8">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path11" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath11">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path12" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath12">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path14" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath15">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 511.63431,224.36663 q -8.98433,0 -13.47647,-5.16598 -4.26755,-5.16599 -5.83981,-15.72256 -1.34765,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49214,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49217,4.94137 6.06443,15.72256 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62679 -1.57226,10.55657 -6.06444,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path16" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath17">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.32926,224.3022 q -8.98433,0 -13.47647,-5.16597 -4.26755,-5.16599 -5.83981,-15.72257 -1.34765,-10.78118 -1.57226,-27.62678 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72257 4.49214,-5.16598 13.47647,-5.16598 8.98432,0 13.25187,5.16598 4.49217,4.94138 6.06443,15.72257 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62678 -1.57226,10.55658 -6.06444,15.72257 -4.49215,5.16597 -13.47647,5.16597 z"
id="path17" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path19" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath19">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path20" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath20">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path21" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path22" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath23">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path25" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath25">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#259eb4;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path26" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath34">
<path
id="path35"
style="fill:#259eb4;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 169.33334,220.13334 c 0,8.46667 -4.23334,16.93333 -33.86667,16.93333 -29.63333,0 -33.86667,-8.46666 -33.86667,-16.93333 0,-12.7 8.46667,-21.16667 33.86667,-21.16667 25.4,0 33.86667,8.46667 33.86667,21.16667 z" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath35">
<path
id="path36"
style="fill:#259eb4;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 715.91343,141.3418 c 0,8.46667 -4.23334,16.93332 -33.86667,16.93332 -29.63333,0 -33.86667,-8.46665 -33.86667,-16.93332 0,-12.7 8.46667,-21.16668 33.86667,-21.16668 25.4,0 33.86667,8.46668 33.86667,21.16668 z" />
</clipPath>
<filter
style="color-interpolation-filters:sRGB"
id="filter61"
x="-0.43233622"
y="-0.31282974"
width="1.8646724"
height="1.6256595">
<feGaussianBlur
stdDeviation="18.492881"
id="feGaussianBlur61" />
</filter>
</defs>
<g
id="layer1"
transform="translate(255.60477,-19.034522)">
<g
id="g71"
transform="matrix(0.53845513,0,0,0.53845513,-260.51955,13.25883)"
inkscape:label="ICON">
<g
id="g4"
inkscape:label="OUTLINE">
<path
id="path1"
style="display:inline;fill:#2d2d2d;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 131.35535,193.41317 c -8.42529,0.21555 -17.07195,1.23898 -24.70899,5.03516 -6.56852,3.28887 -11.780948,9.56904 -13.037575,16.89416 -1.12503,5.50318 -0.847502,11.50687 1.819693,16.54193 2.619641,4.74577 7.282642,8.08459 12.343352,9.82958 7.20278,2.70364 14.97875,3.20323 22.59699,3.46523 7.89843,0.20478 15.85819,-0.17121 23.61105,-1.76836 5.63625,-1.25422 11.31076,-3.51115 15.35105,-7.77992 3.29144,-3.52234 5.05422,-8.32066 5.11255,-13.1185 0.31661,-6.70741 -1.69285,-13.72981 -6.39837,-18.65616 -4.18578,-4.5464 -10.04779,-7.11576 -15.95626,-8.569 -6.77112,-1.66654 -13.79057,-1.92415 -20.73349,-1.87412 z" />
<path
id="path4"
style="display:inline;mix-blend-mode:normal;fill:#2d2d2d;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;stroke-opacity:1;paint-order:stroke fill markers"
d="m 98.902224,36.772549 c -6.995893,0.126233 -13.62619,4.553849 -16.638172,10.834978 -3.470673,6.368967 -4.476788,13.68672 -5.225527,20.799337 -1.546324,15.903589 -0.51861,31.905986 -1.354082,47.842876 -0.544064,12.91133 -1.490643,25.96714 0.724426,38.77934 1.594594,9.15149 5.382372,18.1514 11.945332,24.8552 5.649973,5.83947 12.892719,10.17523 20.761639,12.23377 4.29395,0.94959 9.28733,-0.57303 11.73776,-4.36606 2.38164,-3.59662 2.34796,-8.32076 0.82613,-12.24486 -1.20789,-3.11502 -2.64307,-6.14315 -3.64842,-9.33584 -1.12185,-3.39459 -1.94228,-6.99797 -2.3869,-10.47882 -0.55748,-4.05967 -0.49766,-8.22945 -0.15435,-12.24032 0.58298,-6.27022 1.46357,-12.5645 3.39036,-18.57849 0.79153,-2.26867 1.52928,-4.64049 3.02766,-6.55814 5.54574,0.77098 10.63142,3.47347 15.10865,6.73077 1.80507,1.34294 3.49258,2.79574 5.00233,4.33482 4.14952,4.21383 7.69916,9.25861 9.14438,15.0671 1.23316,4.77068 1.46924,9.85005 0.41871,14.67703 -1.02032,4.50982 -3.25363,8.60065 -5.45452,12.62181 -1.87577,3.646 -3.05511,7.8744 -2.17051,11.96837 0.96209,4.20638 4.82507,7.5927 9.1658,7.8093 4.52117,0.38592 8.79301,-1.6188 12.64582,-3.75368 9.91038,-5.51624 17.57193,-14.7796 21.44242,-25.41236 3.56981,-9.82442 4.74142,-20.45137 3.89537,-30.84701 -1.23706,-11.29301 -6.03879,-21.87272 -12.00583,-31.42104 -8.7119,-13.884894 -20.08721,-25.908588 -32.38582,-36.666079 -9.92644,-8.582365 -20.62052,-16.394651 -32.3549,-22.32888 -4.66406,-2.261816 -9.65523,-4.278849 -14.913576,-4.33312 -0.18139,0.0033 -0.36279,0.0067 -0.54418,0.01 z" />
</g>
<g
id="g26"
clip-path="none"
style="display:inline"
inkscape:label="STUB">
<path
id="path2"
style="fill:#259eb4;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 169.33334,220.13334 c 0,8.46667 -4.23334,16.93333 -33.86667,16.93333 -29.63333,0 -33.86667,-8.46666 -33.86667,-16.93333 0,-12.7 8.46667,-21.16667 33.86667,-21.16667 25.4,0 33.86667,8.46667 33.86667,21.16667 z" />
<path
style="mix-blend-mode:normal;fill:#92cfda;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.292;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;filter:url(#filter27)"
d="m 649.45734,133.81724 c 0.30861,-0.52382 1.24567,-1.10943 2.25778,-1.9344 0.71834,-0.58551 1.60294,-1.1585 2.689,-1.71704 1.33566,-0.68692 2.82604,-1.27591 4.41873,-1.81477 0.91309,-0.30893 1.90318,-0.61043 2.97552,-0.89949 2.75909,-0.74374 5.93213,-1.36936 9.45117,-1.81993 3.4485,-0.44153 7.02191,-0.68799 10.54487,-0.74159 0.11888,-0.002 0.23758,-0.003 0.35611,-0.005 3.42087,-0.0395 6.87572,0.10105 10.20745,0.42751 3.52675,0.34557 6.72,0.8799 9.50577,1.55294 1.13268,0.27365 2.17777,0.56526 3.14025,0.86932 1.5536,0.49082 3.01719,1.0374 4.3387,1.68661 1.10269,0.54172 2.00605,1.10621 2.74422,1.69041 1.03948,0.82267 1.92442,1.4682 2.24848,1.99812 -0.19311,-0.57592 -0.56732,-1.6128 -1.44388,-2.71822 -0.62575,-0.78911 -1.46647,-2.06647 -2.53663,-2.85214 -1.27528,-0.93627 -2.69843,-2.19648 -4.28815,-2.8662 -0.98885,-0.41658 -2.24548,-1.23914 -3.41727,-1.59811 -2.87657,-0.88121 -5.61102,-1.58246 -9.24402,-2.0046 -3.43498,-0.39913 -7.78163,-0.93367 -11.29835,-0.89735 -0.12203,0.001 -0.24424,0.003 -0.36663,0.005 -3.62086,0.0519 -8.29298,0.61115 -11.8461,1.12441 -3.62258,0.5233 -6.54204,1.50557 -9.38537,2.45693 -1.10746,0.37055 -2.4353,1.22472 -3.37108,1.64065 -1.62494,0.72224 -2.75555,1.66679 -4.03455,2.65099 -1.04655,0.80534 -1.65707,2.24134 -2.25723,3.03279 -0.84139,1.10955 -1.24261,2.1654 -1.41747,2.7334 z"
id="path24"
transform="translate(-546.58009,78.791545)"
clip-path="url(#clipPath35)"
sodipodi:nodetypes="csssssccssssscsssssccssssscc" />
<path
style="mix-blend-mode:normal;fill:#134f5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.292;stroke-linecap:round;stroke-linejoin:round;filter:url(#filter60)"
d="m 102.62873,226.09888 c 0.21837,0.59119 0.61147,1.63901 1.62645,2.90953 0.71917,0.90024 1.67343,1.81283 2.88011,2.66136 0.63885,0.44924 1.326,0.86437 2.04583,1.23422 1.73607,0.89199 3.88794,1.6902 6.44451,2.35832 3.15978,0.82576 6.73316,1.3998 10.52375,1.74639 2.77174,0.25344 5.543,0.37487 8.21345,0.40382 1.03453,0.0112 2.07172,0.008 3.10774,-0.0105 3.80268,-0.0675 7.55355,-0.33754 11.05616,-0.82447 3.61058,-0.50195 6.89851,-1.22612 9.69476,-2.17075 0.18263,-0.0617 0.36305,-0.1243 0.54123,-0.1878 2.50252,-0.89183 4.82056,-2.11237 6.51802,-3.52561 0.94359,-0.78561 1.6151,-1.56242 2.06492,-2.2946 0.63371,-1.03151 0.74175,-1.832 0.76927,-2.29791 -0.27946,0.40619 -0.77594,0.89664 -1.67451,1.38662 -0.63117,0.34418 -1.42595,0.67135 -2.41953,0.98728 -1.84319,0.58607 -3.98976,1.02107 -6.42445,1.47239 -0.16947,0.0314 -0.34092,0.0627 -0.51435,0.094 -2.66528,0.48001 -5.74678,0.9266 -9.18723,1.27047 -3.33185,0.33301 -6.89061,0.55814 -10.52391,0.6262 -0.98784,0.0185 -1.97654,0.0253 -2.96263,0.0197 -2.54936,-0.0146 -5.17908,-0.11853 -7.80849,-0.31818 -3.6162,-0.27458 -6.92286,-0.70577 -9.89494,-1.23993 -2.40547,-0.43232 -4.36461,-0.8899 -6.04479,-1.31771 -0.70208,-0.17876 -1.35634,-0.34389 -2.02406,-0.52251 -1.24448,-0.33289 -2.31304,-0.65483 -3.26912,-1.01993 -1.3453,-0.51373 -2.22471,-1.029 -2.73819,-1.44035 z"
id="path59"
clip-path="url(#clipPath34)" />
</g>
<path
style="display:inline;mix-blend-mode:normal;fill:#3ba8bc;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;stroke-opacity:1;paint-order:stroke fill markers"
d="m 101.6,42.333334 c 16.93334,0 84.66667,50.800005 84.66667,93.133336 0,28.57209 -13.39181,41.07402 -25.11117,46.54433 -8.87807,4.14406 -11.33775,-1.382 -6.3999,-9.88879 3.31767,-5.71559 6.11107,-12.63293 6.11107,-19.72221 0,-12.7 -4.23333,-21.16666 -12.7,-29.63333 -8.46667,-8.46667 -19.34216,-12.70001 -25.4,-12.7 -8.46666,1e-5 -12.7,24.8856 -12.7,38.1 0,10.52973 3.14292,20.01182 6.30176,26.88277 2.97912,6.48004 0.48516,10.93086 -6.19604,8.47352 -7.29956,-2.68477 -16.052373,-7.98293 -21.272388,-18.42296 -8.466666,-16.93333 -4.233333,-41.3022 -4.233333,-63.5 0,-33.866665 -7e-6,-59.266666 16.933331,-59.266666 z"
id="path3"
inkscape:label="FLAME" />
<path
style="display:inline;mix-blend-mode:normal;fill:#3ba8bc;fill-opacity:0.151125;stroke:none;stroke-width:2.11667;stroke-linecap:square;stroke-opacity:1;paint-order:stroke fill markers;filter:url(#filter70)"
d="m 101.6,42.333334 c 16.93334,0 84.66667,50.800005 84.66667,93.133336 0,28.57209 -13.39181,41.07402 -25.11117,46.54433 -8.87807,4.14406 -11.33775,-1.382 -6.3999,-9.88879 3.31767,-5.71559 6.11107,-12.63293 6.11107,-19.72221 0,-12.7 -4.23333,-21.16666 -12.7,-29.63333 -8.46667,-8.46667 -19.34216,-12.70001 -25.4,-12.7 -8.46666,1e-5 -12.7,24.8856 -12.7,38.1 0,10.52973 3.14292,20.01182 6.30176,26.88277 2.97912,6.48004 0.48516,10.93086 -6.19604,8.47352 -7.29956,-2.68477 -16.052373,-7.98293 -21.272388,-18.42296 -8.466666,-16.93333 -4.233333,-41.3022 -4.233333,-63.5 0,-33.866665 -7e-6,-59.266666 16.933331,-59.266666 z"
id="path67"
inkscape:label="GLOW" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 30 KiB

-66
View File
@@ -1,66 +0,0 @@
:root > * {
--md-primary-fg-color: #1e97b5;
--md-primary-fg-color--dark: color-mix(in srgb, var(--md-primary-fg-color), black 20%);
--md-accent-fg-color: var(--md-primary-fg-color--dark);
}
.md-logo > img {
background-color: white;
border-radius: 8px;
}
@media (prefers-color-scheme: dark) {
:root > * {
--md-primary-fg-color: #167188;
--md-accent-fg-color: #1e97b5;
--md-typeset-a-color: color-mix(in srgb, var(--md-accent-fg-color), white 30%);
}
.md-logo > img {
background-color: #eee;
}
}
.md-nav--primary .md-nav__title {
box-shadow: none;
}
@media screen and (min-width: 76.25em) {
.md-nav__title {
display: none;
}
}
@media screen and (min-width: 76.25em) {
.md-main {
min-height: 100vh;
}
}
.md-nav__link--active {
font-weight: bold;
}
.highlight .gp, .highlight .go {
user-select: none;
}
.md-source-file__fact {
visibility: hidden;
}
.badge {
font-size: 0.85em;
box-shadow: 0 0 0 1px inset var(--md-accent-fg-color--transparent);
padding: 0.2rem 0.3rem;
}
.md-typeset table:not([class]) td {
padding: .5em 1.25em;
vertical-align: middle;
}
/* Improve keyboard shortcuts for screen readers: this way they won't break on them. */
.md-typeset kbd {
display: inline;
}
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bce4f9debe831beb28bc20ed96f699a4d67a587ecfaf47aff7266b04876b5d3a
size 31393
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b854a03568b2b9505e760cba957d82facfc94138b1f8a2b0c173f80cc6633734
size 8495
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc31d15494c4fefedcad664d79838ae33e6bc3ef9f06b4413f92df4b0dcf374c
size 47121
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5a63ea3cc2f158e175c00dd058988a2bbf676e2a2aac5c2ef1603bd983589d5
size 166777
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bef0c57d617916bf6014fe08e268c8201d7f6ef682e3aea3395e76116b1d0400
size 56936
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f4cc85f606afb37b2b4490f5bdc64ddaf509210bdf59007eb853bb15d5959c4
size 14069
-110
View File
@@ -1,110 +0,0 @@
<svg width="300" height="300" viewBox="0 0 83.343752 83.343748" version="1.1" id="svg1" sodipodi:docname="smol-icon.svg" inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview id="namedview1" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="1" inkscape:zoom="0.8030909" inkscape:cx="63.504642" inkscape:cy="136.9708" inkscape:window-width="2528" inkscape:window-height="1408" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="0" inkscape:current-layer="layer1"/>
<defs id="defs1">
<filter style="color-interpolation-filters:sRGB" id="filter21" x="-0.35129356" y="-0.93287116" width="1.7025871" height="2.8657422">
<feGaussianBlur stdDeviation="5.0792937" id="feGaussianBlur21"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter22" x="-0.42891073" y="-0.64938289" width="1.8578213" height="2.2987657">
<feGaussianBlur stdDeviation="6.5904956" id="feGaussianBlur22"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter23" x="-0.41511482" y="-0.64325017" width="1.8302296" height="2.2865005">
<feGaussianBlur stdDeviation="7.8584288" id="feGaussianBlur23"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter24" x="-0.36140347" y="-0.92623633" width="1.7228069" height="2.8524725">
<feGaussianBlur stdDeviation="5.269127" id="feGaussianBlur24"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter27" x="-0.17576355" y="-0.83601773" width="1.3515271" height="2.6720355">
<feGaussianBlur stdDeviation="4.753432" id="feGaussianBlur26"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter52" x="-0.33025387" y="-1.0968854" width="1.6605078" height="3.1937709">
<feGaussianBlur stdDeviation="4.2575558" id="feGaussianBlur52"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter53" x="-0.34014133" y="-1.0243618" width="1.6802826" height="3.0487237">
<feGaussianBlur stdDeviation="4.3187927" id="feGaussianBlur54"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter55" x="-0.33171228" y="-1.092941" width="1.6634245" height="3.1858823">
<feGaussianBlur stdDeviation="4.4263355" id="feGaussianBlur55"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter56" x="-0.3459667" y="-0.98818076" width="1.6919334" height="2.9763615">
<feGaussianBlur stdDeviation="4.9267071" id="feGaussianBlur56"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter57" x="-0.50604737" y="-0.53225249" width="2.0120947" height="2.0645051">
<feGaussianBlur stdDeviation="5.8261111" id="feGaussianBlur57"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter58" x="-0.32130077" y="-1.1307663" width="1.6426016" height="3.2615325">
<feGaussianBlur stdDeviation="4.2390405" id="feGaussianBlur58"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter59" x="-0.4030115" y="-0.71156067" width="1.806023" height="2.4231212">
<feGaussianBlur stdDeviation="4.4950086" id="feGaussianBlur59"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter60" x="-0.1759294" y="-1.0177407" width="1.3518589" height="3.0354815">
<feGaussianBlur stdDeviation="4.8003951" id="feGaussianBlur60"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter70" x="-0.44225678" y="-0.3197549" width="1.8841637" height="1.6395177">
<feGaussianBlur stdDeviation="18.461288" id="feGaussianBlur70"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter72" x="-0.43271318" y="-0.31310251" width="1.8654264" height="1.626205">
<feGaussianBlur stdDeviation="18.509006" id="feGaussianBlur72"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter1" x="-0.45705071" y="-0.57220238" width="1.9141014" height="2.1444046">
<feGaussianBlur stdDeviation="8.5810337" id="feGaussianBlur1"/>
</filter>
<filter style="color-interpolation-filters:sRGB" id="filter2" x="-0.545201" y="-0.88686216" width="2.0904019" height="2.7737243">
<feGaussianBlur stdDeviation="4.3199889" id="feGaussianBlur2"/>
</filter>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath1">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z" id="path5"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath5">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z" id="path8"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath8">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z" id="path11"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath11">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z" id="path12"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath12">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z" id="path14"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath15">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 511.63431,224.36663 q -8.98433,0 -13.47647,-5.16598 -4.26755,-5.16599 -5.83981,-15.72256 -1.34765,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49214,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49217,4.94137 6.06443,15.72256 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62679 -1.57226,10.55657 -6.06444,15.72256 -4.49215,5.16598 -13.47647,5.16598 z" id="path16"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath17">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 681.32926,224.3022 q -8.98433,0 -13.47647,-5.16597 -4.26755,-5.16599 -5.83981,-15.72257 -1.34765,-10.78118 -1.57226,-27.62678 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72257 4.49214,-5.16598 13.47647,-5.16598 8.98432,0 13.25187,5.16598 4.49217,4.94138 6.06443,15.72257 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62678 -1.57226,10.55658 -6.06444,15.72257 -4.49215,5.16597 -13.47647,5.16597 z" id="path17"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath18">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z" id="path19"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath19">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z" id="path20"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath20">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z" id="path21"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath21">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z" id="path22"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath23">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z" id="path25"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath25">
<path style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z" id="path26"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath34">
<path id="path35" style="fill:#d55c44;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 169.33334,220.13334 c 0,8.46667 -4.23334,16.93333 -33.86667,16.93333 -29.63333,0 -33.86667,-8.46666 -33.86667,-16.93333 0,-12.7 8.46667,-21.16667 33.86667,-21.16667 25.4,0 33.86667,8.46667 33.86667,21.16667 z"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath35">
<path id="path36" style="fill:#d55c44;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers" d="m 715.91343,141.3418 c 0,8.46667 -4.23334,16.93332 -33.86667,16.93332 -29.63333,0 -33.86667,-8.46665 -33.86667,-16.93332 0,-12.7 8.46667,-21.16668 33.86667,-21.16668 25.4,0 33.86667,8.46668 33.86667,21.16668 z"/>
</clipPath>
<filter style="color-interpolation-filters:sRGB" id="filter61" x="-0.43233621" y="-0.31282973" width="1.8646724" height="1.6256595">
<feGaussianBlur stdDeviation="18.492881" id="feGaussianBlur61"/>
</filter>
</defs>
<g id="layer1" transform="translate(255.60477,-19.034522)">
<g id="g71" transform="matrix(0.35676509,0,0,0.35676509,-261.36445,12.212049)" inkscape:label="ICON">
<path id="path2" style="fill:#d55c44;fill-opacity:1;stroke-width:2.03467;stroke-linecap:square;paint-order:stroke fill markers" d="m 166.01229,213.24524 c 0,8.13867 -4.06933,16.27733 -32.55465,16.27733 -28.48531,0 -32.55465,-8.13866 -32.55465,-16.27733 0,-12.20799 8.13867,-20.34665 32.55465,-20.34665 24.41599,0 32.55465,8.13866 32.55465,20.34665 z"/>
<path style="display:inline;mix-blend-mode:normal;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.03467;stroke-linecap:square;stroke-opacity:1;paint-order:stroke fill markers" d="m 100.90299,42.333334 c 16.27733,0 81.38662,48.831978 81.38662,89.525286 0,27.46519 -12.873,39.48278 -24.13834,44.74117 -8.53413,3.98352 -10.89852,-1.32846 -6.15196,-9.50569 3.18914,-5.49417 5.87432,-12.14352 5.87432,-18.95816 0,-12.20799 -4.06933,-20.34665 -12.20799,-28.48531 -8.13867,-8.13867 -18.59284,-12.20801 -24.41599,-12.208 -8.13866,1e-5 -12.20799,23.92152 -12.20799,36.62398 0,10.1218 3.02116,19.23655 6.05762,25.84132 2.86371,6.22899 0.46637,10.50739 -5.956,8.14525 -7.01677,-2.58076 -15.430493,-7.67367 -20.448281,-17.70925 -8.138661,-16.27732 -4.069331,-39.70212 -4.069331,-61.039962 0,-32.554647 -6e-6,-56.970634 16.277322,-56.970634 z" id="path3" inkscape:label="FLAME"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 21 KiB

-418
View File
@@ -1,418 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="300"
height="300"
viewBox="0 0 135.46667 135.46668"
version="1.1"
id="svg1"
sodipodi:docname="processed-icon.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:zoom="1.5347643"
inkscape:cx="193.84084"
inkscape:cy="116.95607"
inkscape:window-width="1653"
inkscape:window-height="1048"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="g71"
pagecolor="#505050"
bordercolor="#ffffff"
inkscape:deskcolor="#505050" />
<defs
id="defs1">
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#d8485f;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#d88748;stop-opacity:1;"
offset="1"
id="stop2" />
</linearGradient>
<filter
style="color-interpolation-filters:sRGB"
id="filter21"
x="-0.35129357"
y="-0.93287114"
width="1.7025871"
height="2.8657423">
<feGaussianBlur
stdDeviation="5.0792937"
id="feGaussianBlur21" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter22"
x="-0.42891072"
y="-0.64938287"
width="1.8578214"
height="2.2987657">
<feGaussianBlur
stdDeviation="6.5904956"
id="feGaussianBlur22" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter23"
x="-0.41511482"
y="-0.64325018"
width="1.8302296"
height="2.2865004">
<feGaussianBlur
stdDeviation="7.8584288"
id="feGaussianBlur23" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter24"
x="-0.36140346"
y="-0.92623631"
width="1.7228069"
height="2.8524726">
<feGaussianBlur
stdDeviation="5.269127"
id="feGaussianBlur24" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter27"
x="-0.3220365"
y="-1.5317638"
width="1.644073"
height="4.0635276">
<feGaussianBlur
stdDeviation="8.7093071"
id="feGaussianBlur26" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter52"
x="-0.33025388"
y="-1.0968854"
width="1.6605078"
height="3.1937708">
<feGaussianBlur
stdDeviation="4.2575558"
id="feGaussianBlur52" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter53"
x="-0.34014132"
y="-1.0243618"
width="1.6802826"
height="3.0487236">
<feGaussianBlur
stdDeviation="4.3187927"
id="feGaussianBlur54" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter55"
x="-0.33171227"
y="-1.0929411"
width="1.6634245"
height="3.1858823">
<feGaussianBlur
stdDeviation="4.4263355"
id="feGaussianBlur55" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter56"
x="-0.34596671"
y="-0.98818076"
width="1.6919334"
height="2.9763615">
<feGaussianBlur
stdDeviation="4.9267071"
id="feGaussianBlur56" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter57"
x="-0.50604737"
y="-0.53225252"
width="2.0120947"
height="2.064505">
<feGaussianBlur
stdDeviation="5.8261111"
id="feGaussianBlur57" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter58"
x="-0.32130078"
y="-1.1307663"
width="1.6426016"
height="3.2615325">
<feGaussianBlur
stdDeviation="4.2390405"
id="feGaussianBlur58" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter59"
x="-0.4030115"
y="-0.71156066"
width="1.806023"
height="2.4231213">
<feGaussianBlur
stdDeviation="4.4950086"
id="feGaussianBlur59" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter60"
x="-0.53873945"
y="-1.1033097"
width="2.0774789"
height="3.2066195">
<feGaussianBlur
stdDeviation="14.7 5.204"
id="feGaussianBlur60" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter70"
x="-0.43159762"
y="-0.3122953"
width="1.8631952"
height="1.6245906">
<feGaussianBlur
stdDeviation="18.461288"
id="feGaussianBlur70" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter72"
x="-0.43271319"
y="-0.31310251"
width="1.8654264"
height="1.626205">
<feGaussianBlur
stdDeviation="18.509006"
id="feGaussianBlur72" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter1"
x="-0.45705071"
y="-0.57220236"
width="1.9141014"
height="2.1444047">
<feGaussianBlur
stdDeviation="8.5810337"
id="feGaussianBlur1" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter2"
x="-0.545201"
y="-0.88686217"
width="2.090402"
height="2.7737243">
<feGaussianBlur
stdDeviation="4.3199889"
id="feGaussianBlur2" />
</filter>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path5" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath5">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path8" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath8">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path11" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath11">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path12" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath12">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path14" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath15">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 511.63431,224.36663 q -8.98433,0 -13.47647,-5.16598 -4.26755,-5.16599 -5.83981,-15.72256 -1.34765,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49214,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49217,4.94137 6.06443,15.72256 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62679 -1.57226,10.55657 -6.06444,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path16" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath17">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.32926,224.3022 q -8.98433,0 -13.47647,-5.16597 -4.26755,-5.16599 -5.83981,-15.72257 -1.34765,-10.78118 -1.57226,-27.62678 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72257 4.49214,-5.16598 13.47647,-5.16598 8.98432,0 13.25187,5.16598 4.49217,4.94138 6.06443,15.72257 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62678 -1.57226,10.55658 -6.06444,15.72257 -4.49215,5.16597 -13.47647,5.16597 z"
id="path17" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path19" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath19">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path20" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath20">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path21" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path22" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath23">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path25" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath25">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#bf4040;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path26" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath34">
<path
id="path35"
style="fill:#bf4040;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 169.33334,220.13334 c 0,8.46667 -4.23334,16.93333 -33.86667,16.93333 -29.63333,0 -33.86667,-8.46666 -33.86667,-16.93333 0,-12.7 8.46667,-21.16667 33.86667,-21.16667 25.4,0 33.86667,8.46667 33.86667,21.16667 z" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath35">
<path
id="path36"
style="fill:#bf4040;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 715.91343,141.3418 c 0,8.46667 -4.23334,16.93332 -33.86667,16.93332 -29.63333,0 -33.86667,-8.46665 -33.86667,-16.93332 0,-12.7 8.46667,-21.16668 33.86667,-21.16668 25.4,0 33.86667,8.46668 33.86667,21.16668 z" />
</clipPath>
<filter
style="color-interpolation-filters:sRGB"
id="filter61"
x="-0.43233622"
y="-0.31282974"
width="1.8646724"
height="1.6256595">
<feGaussianBlur
stdDeviation="18.492881"
id="feGaussianBlur61" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="-3.6298752"
y1="229.95894"
x2="160.69257"
y2="24.819994"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="layer1"
transform="translate(255.60477,-19.034522)">
<g
id="g71"
transform="matrix(0.53845513,0,0,0.53845513,-260.51955,13.25883)"
inkscape:label="ICON">
<g
id="g4"
inkscape:label="OUTLINE">
<path
id="path1"
style="display:inline;fill:#2d2d2d;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 131.35535,193.41317 c -8.42529,0.21555 -17.07195,1.23898 -24.70899,5.03516 -6.56852,3.28887 -11.780948,9.56904 -13.037575,16.89416 -1.12503,5.50318 -0.847502,11.50687 1.819693,16.54193 2.619641,4.74577 7.282642,8.08459 12.343352,9.82958 7.20278,2.70364 14.97875,3.20323 22.59699,3.46523 7.89843,0.20478 15.85819,-0.17121 23.61105,-1.76836 5.63625,-1.25422 11.31076,-3.51115 15.35105,-7.77992 3.29144,-3.52234 5.05422,-8.32066 5.11255,-13.1185 0.31661,-6.70741 -1.69285,-13.72981 -6.39837,-18.65616 -4.18578,-4.5464 -10.04779,-7.11576 -15.95626,-8.569 -6.77112,-1.66654 -13.79057,-1.92415 -20.73349,-1.87412 z" />
<path
id="path4"
style="display:inline;mix-blend-mode:normal;fill:#2d2d2d;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;stroke-opacity:1;paint-order:stroke fill markers"
d="m 98.902224,36.772549 c -6.995893,0.126233 -13.62619,4.553849 -16.638172,10.834978 -3.470673,6.368967 -4.476788,13.68672 -5.225527,20.799337 -1.546324,15.903589 -0.51861,31.905986 -1.354082,47.842876 -0.544064,12.91133 -1.490643,25.96714 0.724426,38.77934 1.594594,9.15149 5.382372,18.1514 11.945332,24.8552 5.649973,5.83947 12.892719,10.17523 20.761639,12.23377 4.29395,0.94959 9.28733,-0.57303 11.73776,-4.36606 2.38164,-3.59662 2.34796,-8.32076 0.82613,-12.24486 -1.20789,-3.11502 -2.64307,-6.14315 -3.64842,-9.33584 -1.12185,-3.39459 -1.94228,-6.99797 -2.3869,-10.47882 -0.55748,-4.05967 -0.49766,-8.22945 -0.15435,-12.24032 0.58298,-6.27022 1.46357,-12.5645 3.39036,-18.57849 0.79153,-2.26867 1.52928,-4.64049 3.02766,-6.55814 5.54574,0.77098 10.63142,3.47347 15.10865,6.73077 1.80507,1.34294 3.49258,2.79574 5.00233,4.33482 4.14952,4.21383 7.69916,9.25861 9.14438,15.0671 1.23316,4.77068 1.46924,9.85005 0.41871,14.67703 -1.02032,4.50982 -3.25363,8.60065 -5.45452,12.62181 -1.87577,3.646 -3.05511,7.8744 -2.17051,11.96837 0.96209,4.20638 4.82507,7.5927 9.1658,7.8093 4.52117,0.38592 8.79301,-1.6188 12.64582,-3.75368 9.91038,-5.51624 17.57193,-14.7796 21.44242,-25.41236 3.56981,-9.82442 4.74142,-20.45137 3.89537,-30.84701 -1.23706,-11.29301 -6.03879,-21.87272 -12.00583,-31.42104 -8.7119,-13.884894 -20.08721,-25.908588 -32.38582,-36.666079 -9.92644,-8.582365 -20.62052,-16.394651 -32.3549,-22.32888 -4.66406,-2.261816 -9.65523,-4.278849 -14.913576,-4.33312 -0.18139,0.0033 -0.36279,0.0067 -0.54418,0.01 z" />
</g>
<path
id="path2"
style="display:inline;fill:url(#linearGradient2);fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="M 101.5994 42.333286 C 84.666067 42.333286 84.666625 67.732989 84.666625 101.59965 C 84.666625 123.79745 80.433973 148.16671 88.900639 165.10004 C 94.120654 175.54007 102.8727 180.83855 110.17226 183.52332 C 116.85346 185.98066 119.34762 181.52878 116.3685 175.04874 C 113.20966 168.17779 110.06743 158.69699 110.06743 148.16726 C 110.06743 134.95286 114.29954 110.06605 122.7662 110.06604 C 128.82404 110.06603 139.70034 114.29978 148.16701 122.76645 C 156.63368 131.23312 160.86741 139.69963 160.86741 152.39963 C 160.86741 159.48891 158.07401 166.40619 154.75634 172.12178 C 149.81849 180.62857 152.27761 186.15558 161.15568 182.01152 C 172.87504 176.54121 186.26658 164.03894 186.26658 135.46685 C 186.26658 93.13352 118.53274 42.333286 101.5994 42.333286 z M 135.4666 198.96723 C 110.0666 198.96723 101.5994 207.43403 101.5994 220.13403 C 101.5994 228.6007 105.83327 237.06681 135.4666 237.06681 C 165.09993 237.06681 169.3338 228.6007 169.3338 220.13403 C 169.3338 207.43403 160.8666 198.96723 135.4666 198.96723 z " />
<g
id="g26"
clip-path="none"
style="display:inline"
inkscape:label="STUB">
<path
style="mix-blend-mode:normal;fill:#ffffff;fill-opacity:0.745813;fill-rule:nonzero;stroke:none;stroke-width:1.292;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;filter:url(#filter27)"
d="m 649.45734,133.81724 c 0.30861,-0.52382 1.24567,-1.10943 2.25778,-1.9344 0.71834,-0.58551 1.60294,-1.1585 2.689,-1.71704 1.33566,-0.68692 2.82604,-1.27591 4.41873,-1.81477 0.91309,-0.30893 1.90318,-0.61043 2.97552,-0.89949 2.75909,-0.74374 5.93213,-1.36936 9.45117,-1.81993 3.4485,-0.44153 7.02191,-0.68799 10.54487,-0.74159 0.11888,-0.002 0.23758,-0.003 0.35611,-0.005 3.42087,-0.0395 6.87572,0.10105 10.20745,0.42751 3.52675,0.34557 6.72,0.8799 9.50577,1.55294 1.13268,0.27365 2.17777,0.56526 3.14025,0.86932 1.5536,0.49082 3.01719,1.0374 4.3387,1.68661 1.10269,0.54172 2.00605,1.10621 2.74422,1.69041 1.03948,0.82267 1.92442,1.4682 2.24848,1.99812 -0.19311,-0.57592 -0.56732,-1.6128 -1.44388,-2.71822 -0.62575,-0.78911 -1.46647,-2.06647 -2.53663,-2.85214 -1.27528,-0.93627 -2.69843,-2.19648 -4.28815,-2.8662 -0.98885,-0.41658 -2.24548,-1.23914 -3.41727,-1.59811 -2.87657,-0.88121 -5.61102,-1.58246 -9.24402,-2.0046 -3.43498,-0.39913 -7.78163,-0.93367 -11.29835,-0.89735 -0.12203,0.001 -0.24424,0.003 -0.36663,0.005 -3.62086,0.0519 -8.29298,0.61115 -11.8461,1.12441 -3.62258,0.5233 -6.54204,1.50557 -9.38537,2.45693 -1.10746,0.37055 -2.4353,1.22472 -3.37108,1.64065 -1.62494,0.72224 -2.75555,1.66679 -4.03455,2.65099 -1.04655,0.80534 -1.65707,2.24134 -2.25723,3.03279 -0.84139,1.10955 -1.24261,2.1654 -1.41747,2.7334 z"
id="path24"
transform="translate(-546.58009,78.791545)"
clip-path="url(#clipPath35)"
sodipodi:nodetypes="csssssccssssscsssssccssssscc" />
<path
style="mix-blend-mode:normal;fill:#000000;fill-opacity:0.747711;fill-rule:nonzero;stroke:none;stroke-width:1.292;stroke-linecap:round;stroke-linejoin:round;filter:url(#filter60)"
d="m 102.62873,226.09888 c 0.21837,0.59119 0.61147,1.63901 1.62645,2.90953 0.71917,0.90024 1.67343,1.81283 2.88011,2.66136 0.63885,0.44924 1.326,0.86437 2.04583,1.23422 1.73607,0.89199 3.88794,1.6902 6.44451,2.35832 3.15978,0.82576 6.73316,1.3998 10.52375,1.74639 2.77174,0.25344 5.543,0.37487 8.21345,0.40382 1.03453,0.0112 2.07172,0.008 3.10774,-0.0105 3.80268,-0.0675 7.55355,-0.33754 11.05616,-0.82447 3.61058,-0.50195 6.89851,-1.22612 9.69476,-2.17075 0.18263,-0.0617 0.36305,-0.1243 0.54123,-0.1878 2.50252,-0.89183 4.82056,-2.11237 6.51802,-3.52561 0.94359,-0.78561 1.6151,-1.56242 2.06492,-2.2946 0.63371,-1.03151 0.74175,-1.832 0.76927,-2.29791 -0.27946,0.40619 -0.77594,0.89664 -1.67451,1.38662 -0.63117,0.34418 -1.42595,0.67135 -2.41953,0.98728 -1.84319,0.58607 -3.98976,1.02107 -6.42445,1.47239 -0.16947,0.0314 -0.34092,0.0627 -0.51435,0.094 -2.66528,0.48001 -5.74678,0.9266 -9.18723,1.27047 -3.33185,0.33301 -6.89061,0.55814 -10.52391,0.6262 -0.98784,0.0185 -1.97654,0.0253 -2.96263,0.0197 -2.54936,-0.0146 -5.17908,-0.11853 -7.80849,-0.31818 -3.6162,-0.27458 -6.92286,-0.70577 -9.89494,-1.23993 -2.40547,-0.43232 -4.36461,-0.8899 -6.04479,-1.31771 -0.70208,-0.17876 -1.35634,-0.34389 -2.02406,-0.52251 -1.24448,-0.33289 -2.31304,-0.65483 -3.26912,-1.01993 -1.3453,-0.51373 -2.22471,-1.029 -2.73819,-1.44035 z"
id="path59"
clip-path="url(#clipPath34)" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 30 KiB

-391
View File
@@ -1,391 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="253.46544"
height="134.92722"
viewBox="0 0 123.86632 37.866682"
version="1.1"
id="svg1"
sodipodi:docname="niri-logo-smol.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
inkscape:export-filename="niri-logo-smol.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:zoom="0.2284248"
inkscape:cx="76.611646"
inkscape:cy="89.745071"
inkscape:window-width="990"
inkscape:window-height="1048"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"
pagecolor="#ffffff"
bordercolor="#000000"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs1">
<filter
style="color-interpolation-filters:sRGB"
id="filter21"
x="-0.35129356"
y="-0.93287116"
width="1.7025871"
height="2.8657422">
<feGaussianBlur
stdDeviation="5.0792937"
id="feGaussianBlur21" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter22"
x="-0.42891073"
y="-0.64938289"
width="1.8578213"
height="2.2987657">
<feGaussianBlur
stdDeviation="6.5904956"
id="feGaussianBlur22" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter23"
x="-0.41511482"
y="-0.64325017"
width="1.8302296"
height="2.2865005">
<feGaussianBlur
stdDeviation="7.8584288"
id="feGaussianBlur23" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter24"
x="-0.36140347"
y="-0.92623633"
width="1.7228069"
height="2.8524725">
<feGaussianBlur
stdDeviation="5.269127"
id="feGaussianBlur24" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter27"
x="-0.30853596"
y="-1.6721658"
width="1.6170719"
height="4.3443317">
<feGaussianBlur
stdDeviation="8.2543341"
id="feGaussianBlur26" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter52"
x="-0.33025387"
y="-1.0968854"
width="1.6605078"
height="3.1937709">
<feGaussianBlur
stdDeviation="4.2575558"
id="feGaussianBlur52" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter53"
x="-0.34014133"
y="-1.0243618"
width="1.6802826"
height="3.0487237">
<feGaussianBlur
stdDeviation="4.3187927"
id="feGaussianBlur54" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter55"
x="-0.33171228"
y="-1.092941"
width="1.6634245"
height="3.1858823">
<feGaussianBlur
stdDeviation="4.4263355"
id="feGaussianBlur55" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter56"
x="-0.3459667"
y="-0.98818076"
width="1.6919334"
height="2.9763615">
<feGaussianBlur
stdDeviation="4.9267071"
id="feGaussianBlur56" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter57"
x="-0.50604737"
y="-0.53225249"
width="2.0120947"
height="2.0645051">
<feGaussianBlur
stdDeviation="5.8261111"
id="feGaussianBlur57" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter58"
x="-0.32130077"
y="-1.1307663"
width="1.6426016"
height="3.2615325">
<feGaussianBlur
stdDeviation="4.2390405"
id="feGaussianBlur58" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter59"
x="-0.4030115"
y="-0.71156067"
width="1.806023"
height="2.4231212">
<feGaussianBlur
stdDeviation="4.4950086"
id="feGaussianBlur59" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter60"
x="-0.2985791"
y="-1.7272615"
width="1.5971582"
height="4.4545231">
<feGaussianBlur
stdDeviation="8.1470038"
id="feGaussianBlur60" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter70"
x="-0.44225678"
y="-0.3197549"
width="1.8841637"
height="1.6395177">
<feGaussianBlur
stdDeviation="18.461288"
id="feGaussianBlur70" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter72"
x="-0.43271318"
y="-0.31310251"
width="1.8654264"
height="1.626205">
<feGaussianBlur
stdDeviation="18.509006"
id="feGaussianBlur72" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter1"
x="-0.45705071"
y="-0.57220238"
width="1.9141014"
height="2.1444046">
<feGaussianBlur
stdDeviation="8.5810337"
id="feGaussianBlur1" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter2"
x="-0.545201"
y="-0.88686216"
width="2.0904019"
height="2.7737243">
<feGaussianBlur
stdDeviation="4.3199889"
id="feGaussianBlur2" />
</filter>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path5" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath5">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path8" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath8">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path11" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath11">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path12" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath12">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 452.78682,224.36663 q -8.98432,0 -12.80266,-4.94138 -3.59373,-5.16598 -4.26757,-12.80265 -0.67381,-7.86128 -0.67381,-15.72256 0,-11.90423 -3.36913,-19.09168 -3.36912,-7.41207 -11.90421,-7.41207 -7.63667,0 -11.2304,7.41207 -3.59373,7.18745 -3.59373,19.09168 0,7.86128 -0.67384,15.49795 -0.44921,7.63667 -4.04294,12.80266 -3.59373,4.94137 -13.02726,4.94137 -6.96286,0 -11.67961,-5.16598 -4.49217,-5.16599 -6.96285,-13.47648 -2.47068,-8.3105 -3.59373,-17.96864 -0.89842,-9.65815 -0.89842,-18.64247 0,-8.98432 0.89842,-18.41785 0.89844,-9.43354 3.14452,-17.51943 2.47068,-8.08588 6.96285,-13.02726 4.49215,-4.94138 11.67961,-4.94138 12.80266,0 15.94715,11.67962 2.69531,-2.9199 8.53512,-5.39059 6.0644,-2.6953 13.70107,-2.6953 17.29484,0 28.30063,8.75971 11.00577,8.75972 15.94715,24.25767 5.16599,15.27334 4.94139,35.48806 0,10.33197 -2.02147,18.86707 -1.79687,8.3105 -6.51365,13.47648 -4.49215,4.94138 -12.80263,4.94138 z"
id="path14" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath15">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 511.63431,224.36663 q -8.98433,0 -13.47647,-5.16598 -4.26755,-5.16599 -5.83981,-15.72256 -1.34765,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49214,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49217,4.94137 6.06443,15.72256 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62679 -1.57226,10.55657 -6.06444,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path16" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath17">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.32926,224.3022 q -8.98433,0 -13.47647,-5.16597 -4.26755,-5.16599 -5.83981,-15.72257 -1.34765,-10.78118 -1.57226,-27.62678 0,-16.8456 1.34763,-27.62678 1.57226,-10.78119 6.06444,-15.72257 4.49214,-5.16598 13.47647,-5.16598 8.98432,0 13.25187,5.16598 4.49217,4.94138 6.06443,15.72257 1.57223,10.78118 1.57223,27.62678 0.22461,16.8456 -1.34762,27.62678 -1.57226,10.55658 -6.06444,15.72257 -4.49215,5.16597 -13.47647,5.16597 z"
id="path17" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path19" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath19">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path20" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath20">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path21" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 573.177,224.36663 q -7.63667,0 -12.57805,-4.94138 -4.71678,-5.16598 -7.18746,-13.70108 -2.47068,-8.53511 -3.36913,-19.5409 -0.89842,-11.00579 -0.89842,-22.68541 0,-7.86128 0.22461,-15.94717 0.44921,-8.31049 2.24607,-15.27334 2.02147,-6.96285 6.51362,-11.2304 4.71678,-4.49216 13.25187,-4.49216 6.06443,0 11.00582,2.9199 4.94136,2.6953 6.28901,8.3105 5.16599,-4.71677 13.02727,-6.73824 8.08587,-2.24608 16.62099,-2.24608 7.18746,0 14.59952,3.59373 7.41207,3.36912 12.57803,9.43353 5.16599,6.06442 5.16599,13.70109 0,6.51363 -4.94136,12.35344 -4.71678,5.83981 -13.4765,5.83981 -3.81831,0 -6.28901,-0.89843 -2.47068,-1.12304 -4.71676,-2.02147 -2.2461,-0.89844 -5.39059,-0.89844 -8.31051,0 -15.27337,6.73824 -6.73822,6.51364 -6.73822,17.74404 0,5.8398 -0.44921,12.80265 -0.22463,6.73824 -2.02147,13.02727 -1.57226,6.28902 -5.83983,10.33196 -4.26754,3.81834 -12.35342,3.81834 z"
id="path22" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath23">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path25" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath25">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 681.43807,224.36663 q -8.98432,0 -13.47647,-5.16598 -4.26757,-5.16599 -5.83983,-15.72256 -1.34763,-10.78119 -1.57226,-27.62679 0,-16.8456 1.34765,-27.62678 1.57226,-10.78119 6.06444,-15.72256 4.49215,-5.16599 13.47647,-5.16599 8.98432,0 13.25187,5.16599 4.49215,4.94137 6.06441,15.72256 1.57226,10.78118 1.57226,27.62678 0.2246,16.8456 -1.34766,27.62679 -1.57223,10.55657 -6.06441,15.72256 -4.49215,5.16598 -13.47647,5.16598 z"
id="path26" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath34">
<path
id="path35"
style="fill:#d55c44;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 169.33334,220.13334 c 0,8.46667 -4.23334,16.93333 -33.86667,16.93333 -29.63333,0 -33.86667,-8.46666 -33.86667,-16.93333 0,-12.7 8.46667,-21.16667 33.86667,-21.16667 25.4,0 33.86667,8.46667 33.86667,21.16667 z" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath35">
<path
id="path36"
style="fill:#d55c44;fill-opacity:1;stroke-width:2.11667;stroke-linecap:square;paint-order:stroke fill markers"
d="m 715.91343,141.3418 c 0,8.46667 -4.23334,16.93332 -33.86667,16.93332 -29.63333,0 -33.86667,-8.46665 -33.86667,-16.93332 0,-12.7 8.46667,-21.16668 33.86667,-21.16668 25.4,0 33.86667,8.46668 33.86667,21.16668 z" />
</clipPath>
<filter
style="color-interpolation-filters:sRGB"
id="filter61"
x="-0.43233621"
y="-0.31282973"
width="1.8646724"
height="1.6256595">
<feGaussianBlur
stdDeviation="18.492881"
id="feGaussianBlur61" />
</filter>
</defs>
<g
id="layer1"
transform="translate(151.57,-212.41655)">
<g
id="g69"
transform="matrix(1.1313091,0,0,1.1313091,-521.46052,171.60178)"
style="fill:#d55c44;fill-opacity:1">
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:0.673514;stroke-linecap:square;paint-order:stroke fill markers"
d="m 427.944,81.955473 q -2.85876,0 -4.28814,-1.64378 -1.35793,-1.6438 -1.85821,-5.00284 -0.42881,-3.43053 -0.50028,-8.79071 0,-5.36019 0.42881,-8.79071 0.50029,-3.43053 1.92968,-5.00285 1.42938,-1.64379 4.28814,-1.64379 2.85877,0 4.21669,1.64379 1.42938,1.57232 1.92966,5.00285 0.50029,3.43052 0.50029,8.79071 0.0715,5.36018 -0.42882,8.79071 -0.50028,3.35904 -1.92966,5.00284 -1.42939,1.64378 -4.28816,1.64378 z"
id="path63" />
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:0.673514;stroke-linecap:square;paint-order:stroke fill markers"
d="m 393.49586,81.955473 q -2.42995,0 -4.00228,-1.57232 -1.50085,-1.64379 -2.28701,-4.35961 -0.78616,-2.71584 -1.07204,-6.21782 -0.28588,-3.50199 -0.28588,-7.21839 0,-2.50142 0.0715,-5.07431 0.14294,-2.64436 0.71469,-4.8599 0.64323,-2.21555 2.07261,-3.57346 1.50085,-1.42938 4.21668,-1.42938 1.92967,0 3.50199,0.92909 1.57232,0.85763 2.00114,2.64436 1.64378,-1.50085 4.1452,-2.14407 2.5729,-0.71469 5.28873,-0.71469 2.28701,0 4.6455,1.1435 2.35848,1.07204 4.00227,3.00171 1.64379,1.92966 1.64379,4.35962 0,2.0726 -1.57233,3.9308 -1.50085,1.8582 -4.28814,1.8582 -1.21498,0 -2.00114,-0.28588 -0.78616,-0.35734 -1.50086,-0.64322 -0.71469,-0.28588 -1.71525,-0.28588 -2.64436,0 -4.85991,2.14408 -2.14408,2.07261 -2.14408,5.64607 0,1.85819 -0.14293,4.07373 -0.0715,2.14408 -0.64322,4.14522 -0.50029,2.00114 -1.8582,3.28758 -1.35792,1.21497 -3.9308,1.21497 z"
id="path64" />
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:0.673514;stroke-linecap:square;paint-order:stroke fill markers"
d="m 373.91327,81.955473 q -2.85877,0 -4.28815,-1.64378 -1.35791,-1.6438 -1.8582,-5.00284 -0.42881,-3.43053 -0.50028,-8.79071 0,-5.36019 0.42881,-8.79071 0.50029,-3.43053 1.92967,-5.00285 1.42938,-1.64379 4.28815,-1.64379 2.85877,0 4.21668,1.64379 1.42938,1.57232 1.92967,5.00285 0.50029,3.43052 0.50029,8.79071 0.0715,5.36018 -0.42882,8.79071 -0.50029,3.35904 -1.92967,5.00284 -1.42938,1.64378 -4.28815,1.64378 z"
id="path65" />
<path
style="font-size:224.608px;line-height:393.415px;font-family:'Cherry Bomb One';-inkscape-font-specification:'Cherry Bomb One';letter-spacing:0px;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:0.673514;stroke-linecap:square;paint-order:stroke fill markers"
d="m 355.18829,81.955473 q -2.85878,0 -4.07375,-1.57232 -1.14351,-1.64379 -1.35791,-4.07374 -0.21441,-2.50142 -0.21441,-5.00284 0,-3.78787 -1.07204,-6.07488 -1.07204,-2.35848 -3.78786,-2.35848 -2.42996,0 -3.57347,2.35848 -1.1435,2.28701 -1.1435,6.07488 0,2.50142 -0.21441,4.93137 -0.14294,2.42995 -1.28644,4.07375 -1.14351,1.57232 -4.14522,1.57232 -2.21554,0 -3.71639,-1.64379 -1.42939,-1.6438 -2.21554,-4.28815 -0.78617,-2.64437 -1.14352,-5.71754 -0.28587,-3.07317 -0.28587,-5.93194 0,-2.85877 0.28587,-5.86047 0.28588,-3.0017 1.00058,-5.5746 0.78616,-2.57289 2.21554,-4.14521 1.42938,-1.57232 3.71639,-1.57232 4.07375,0 5.07432,3.7164 0.85763,-0.9291 2.71583,-1.71526 1.92967,-0.85763 4.35962,-0.85763 5.50312,0 9.00511,2.78729 3.50199,2.78731 5.07431,7.71868 1.64379,4.8599 1.57233,11.29212 0,3.28759 -0.64323,6.00341 -0.57175,2.64436 -2.07261,4.28815 -1.42938,1.57232 -4.07373,1.57232 z"
id="path66" />
<path
style="display:inline;mix-blend-mode:normal;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:4.02897;stroke-linecap:square;paint-order:stroke fill markers"
d="m 101.6,42.333334 c 16.93334,0 84.66667,50.800005 84.66667,93.133336 0,28.57209 -13.39181,41.07402 -25.11117,46.54433 -8.87807,4.14406 -11.33775,-1.382 -6.3999,-9.88879 3.31767,-5.71559 6.11107,-12.63293 6.11107,-19.72221 0,-12.7 -4.23333,-21.16666 -12.7,-29.63333 -8.46667,-8.46667 -19.34216,-12.70001 -25.4,-12.7 -8.46666,1e-5 -12.7,24.8856 -12.7,38.1 0,10.52973 3.14292,20.01182 6.30176,26.88277 2.97912,6.48004 0.48516,10.93086 -6.19604,8.47352 -7.29956,-2.68477 -16.052373,-7.98293 -21.272388,-18.42296 -8.466666,-16.93333 -4.233333,-41.3022 -4.233333,-63.5 0,-33.866665 -7e-6,-59.266666 16.933331,-59.266666 z"
id="path61"
transform="matrix(0.16716789,0,0,0.16716789,351.42847,16.594243)" />
<path
style="display:inline;mix-blend-mode:normal;fill:#d55c44;fill-opacity:1;stroke:none;stroke-width:4.02897;stroke-linecap:square;paint-order:stroke fill markers"
d="m 101.6,42.333334 c 16.93334,0 84.66667,50.800005 84.66667,93.133336 0,28.57209 -13.39181,41.07402 -25.11117,46.54433 -8.87807,4.14406 -11.33775,-1.382 -6.3999,-9.88879 3.31767,-5.71559 6.11107,-12.63293 6.11107,-19.72221 0,-12.7 -4.23333,-21.16666 -12.7,-29.63333 -8.46667,-8.46667 -19.34216,-12.70001 -25.4,-12.7 -8.46666,1e-5 -12.7,24.8856 -12.7,38.1 0,10.52973 3.14292,20.01182 6.30176,26.88277 2.97912,6.48004 0.48516,10.93086 -6.19604,8.47352 -7.29956,-2.68477 -16.052373,-7.98293 -21.272388,-18.42296 -8.466666,-16.93333 -4.233333,-41.3022 -4.233333,-63.5 0,-33.866665 -7e-6,-59.266666 16.933331,-59.266666 z"
id="path62"
transform="matrix(0.16716789,0,0,0.16716789,405.30952,16.594243)" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

Generated
+22 -6
View File
@@ -1,12 +1,27 @@
{
"nodes": {
"nix-filter": {
"locked": {
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1757967192,
"narHash": "sha256-/aA9A/OBmnuOMgwfzdsXRusqzUpd8rQnQY8jtrHK+To=",
"lastModified": 1742707865,
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0d7c15863b251a7a50265e57c1dca1a7add2e291",
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
"type": "github"
},
"original": {
@@ -18,6 +33,7 @@
},
"root": {
"inputs": {
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
@@ -29,11 +45,11 @@
]
},
"locked": {
"lastModified": 1757989933,
"narHash": "sha256-9cpKYWWPCFhgwQTww8S94rTXgg8Q8ydFv9fXM6I8xQM=",
"lastModified": 1742697269,
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8249aa3442fb9b45e615a35f39eca2fe5510d7c3",
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
"type": "github"
},
"original": {
+15 -23
View File
@@ -4,6 +4,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nix-filter.url = "github:numtide/nix-filter";
# NOTE: This is not necessary for end users
# You can omit it with `inputs.rust-overlay.follows = ""`
@@ -17,10 +18,10 @@
{
self,
nixpkgs,
nix-filter,
rust-overlay,
}:
let
revision = self.shortRev or self.dirtyShortRev or "unknown";
niri-package =
{
lib,
@@ -47,25 +48,25 @@
rustPlatform.buildRustPackage {
pname = "niri";
version = revision;
version = self.shortRev or self.dirtyShortRev or "unknown";
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./niri-config
./niri-ipc
./niri-visual-tests
./resources
./src
./Cargo.toml
src = nix-filter.lib.filter {
root = self;
include = [
"niri-config"
"niri-ipc"
"niri-visual-tests"
"resources"
"src"
./Cargo.lock
./Cargo.toml
];
};
postPatch = ''
patchShebangs resources/niri-session
substituteInPlace resources/niri.service \
--replace-fail 'ExecStart=niri' "ExecStart=$out/bin/niri"
--replace-fail '/usr/bin' "$out/bin"
'';
cargoLock = {
@@ -108,7 +109,7 @@
buildNoDefaultFeatures = true;
# ever since this commit:
# https://github.com/niri-wm/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
# and thus creates a real socket file in the runtime dir.
# this is fine for our build, we just need to make sure it has a directory to write to.
@@ -116,18 +117,11 @@
export XDG_RUNTIME_DIR="$(mktemp -d)"
'';
checkFlags = [
# These tests require the ability to access a "valid EGL Display", but that won't work
# inside the Nix sandbox
"--skip=::egl"
];
postInstall =
''
installShellCompletion --cmd niri \
--bash <($out/bin/niri completions bash) \
--fish <($out/bin/niri completions fish) \
--nushell <($out/bin/niri completions nushell) \
--zsh <($out/bin/niri completions zsh)
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
@@ -149,7 +143,6 @@
"-Wl,--pop-state"
]
);
NIRI_BUILD_COMMIT = revision;
};
passthru = {
@@ -158,7 +151,7 @@
meta = {
description = "Scrollable-tiling Wayland compositor";
homepage = "https://github.com/niri-wm/niri";
homepage = "https://github.com/YaLTeR/niri";
license = lib.licenses.gpl3Only;
mainProgram = "niri";
platforms = lib.platforms.linux;
@@ -207,7 +200,6 @@
];
}
))
pkgs.cargo-insta
];
nativeBuildInputs = [
+3 -4
View File
@@ -9,11 +9,11 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.8.3"
csscolorparser = "0.7.0"
knuffel = "3.2.0"
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
niri-ipc = { version = "26.4.0", path = "../niri-ipc" }
regex = "1.12.3"
niri-ipc = { version = "25.2.0", path = "../niri-ipc" }
regex = "1.11.1"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
@@ -21,4 +21,3 @@ tracy-client.workspace = true
[dev-dependencies]
insta.workspace = true
pretty_assertions = "1.4.1"
diff = "0.1.13"
-837
View File
@@ -1,837 +0,0 @@
use knuffel::errors::DecodeError;
use knuffel::Decode as _;
use crate::utils::{expect_only_children, parse_arg_node, MergeWith};
use crate::FloatOrInt;
#[derive(Debug, Clone, PartialEq)]
pub struct Animations {
pub off: bool,
pub slowdown: f64,
pub workspace_switch: WorkspaceSwitchAnim,
pub window_open: WindowOpenAnim,
pub window_close: WindowCloseAnim,
pub horizontal_view_movement: HorizontalViewMovementAnim,
pub window_movement: WindowMovementAnim,
pub window_resize: WindowResizeAnim,
pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
pub exit_confirmation_open_close: ExitConfirmationOpenCloseAnim,
pub screenshot_ui_open: ScreenshotUiOpenAnim,
pub overview_open_close: OverviewOpenCloseAnim,
pub recent_windows_close: RecentWindowsCloseAnim,
}
impl Default for Animations {
fn default() -> Self {
Self {
off: false,
slowdown: 1.,
workspace_switch: Default::default(),
horizontal_view_movement: Default::default(),
window_movement: Default::default(),
window_open: Default::default(),
window_close: Default::default(),
window_resize: Default::default(),
config_notification_open_close: Default::default(),
exit_confirmation_open_close: Default::default(),
screenshot_ui_open: Default::default(),
overview_open_close: Default::default(),
recent_windows_close: Default::default(),
}
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct AnimationsPart {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub on: bool,
#[knuffel(child, unwrap(argument))]
pub slowdown: Option<FloatOrInt<0, { i32::MAX }>>,
#[knuffel(child)]
pub workspace_switch: Option<WorkspaceSwitchAnim>,
#[knuffel(child)]
pub window_open: Option<WindowOpenAnim>,
#[knuffel(child)]
pub window_close: Option<WindowCloseAnim>,
#[knuffel(child)]
pub horizontal_view_movement: Option<HorizontalViewMovementAnim>,
#[knuffel(child)]
pub window_movement: Option<WindowMovementAnim>,
#[knuffel(child)]
pub window_resize: Option<WindowResizeAnim>,
#[knuffel(child)]
pub config_notification_open_close: Option<ConfigNotificationOpenCloseAnim>,
#[knuffel(child)]
pub exit_confirmation_open_close: Option<ExitConfirmationOpenCloseAnim>,
#[knuffel(child)]
pub screenshot_ui_open: Option<ScreenshotUiOpenAnim>,
#[knuffel(child)]
pub overview_open_close: Option<OverviewOpenCloseAnim>,
#[knuffel(child)]
pub recent_windows_close: Option<RecentWindowsCloseAnim>,
}
impl MergeWith<AnimationsPart> for Animations {
fn merge_with(&mut self, part: &AnimationsPart) {
self.off |= part.off;
if part.on {
self.off = false;
}
merge!((self, part), slowdown);
// Animation properties are fairly tied together, except maybe `off`. So let's just save
// ourselves the work and not merge within individual animations.
merge_clone!(
(self, part),
workspace_switch,
window_open,
window_close,
horizontal_view_movement,
window_movement,
window_resize,
config_notification_open_close,
exit_confirmation_open_close,
screenshot_ui_open,
overview_open_close,
recent_windows_close,
);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Animation {
pub off: bool,
pub kind: Kind,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Kind {
Easing(EasingParams),
Spring(SpringParams),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EasingParams {
pub duration_ms: u32,
pub curve: Curve,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Curve {
Linear,
EaseOutQuad,
EaseOutCubic,
EaseOutExpo,
CubicBezier(f64, f64, f64, f64),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpringParams {
pub damping_ratio: f64,
pub stiffness: u32,
pub epsilon: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WorkspaceSwitchAnim(pub Animation);
impl Default for WorkspaceSwitchAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 1000,
epsilon: 0.0001,
}),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WindowOpenAnim {
pub anim: Animation,
pub custom_shader: Option<String>,
}
impl Default for WindowOpenAnim {
fn default() -> Self {
Self {
anim: Animation {
off: false,
kind: Kind::Easing(EasingParams {
duration_ms: 150,
curve: Curve::EaseOutExpo,
}),
},
custom_shader: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WindowCloseAnim {
pub anim: Animation,
pub custom_shader: Option<String>,
}
impl Default for WindowCloseAnim {
fn default() -> Self {
Self {
anim: Animation {
off: false,
kind: Kind::Easing(EasingParams {
duration_ms: 150,
curve: Curve::EaseOutQuad,
}),
},
custom_shader: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HorizontalViewMovementAnim(pub Animation);
impl Default for HorizontalViewMovementAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WindowMovementAnim(pub Animation);
impl Default for WindowMovementAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WindowResizeAnim {
pub anim: Animation,
pub custom_shader: Option<String>,
}
impl Default for WindowResizeAnim {
fn default() -> Self {
Self {
anim: Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
},
custom_shader: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConfigNotificationOpenCloseAnim(pub Animation);
impl Default for ConfigNotificationOpenCloseAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 0.6,
stiffness: 1000,
epsilon: 0.001,
}),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ExitConfirmationOpenCloseAnim(pub Animation);
impl Default for ExitConfirmationOpenCloseAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 0.6,
stiffness: 500,
epsilon: 0.01,
}),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScreenshotUiOpenAnim(pub Animation);
impl Default for ScreenshotUiOpenAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Easing(EasingParams {
duration_ms: 200,
curve: Curve::EaseOutQuad,
}),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OverviewOpenCloseAnim(pub Animation);
impl Default for OverviewOpenCloseAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RecentWindowsCloseAnim(pub Animation);
impl Default for RecentWindowsCloseAnim {
fn default() -> Self {
Self(Animation {
off: false,
kind: Kind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.001,
}),
})
}
}
impl<S> knuffel::Decode<S> for WorkspaceSwitchAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl<S> knuffel::Decode<S> for HorizontalViewMovementAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl<S> knuffel::Decode<S> for WindowMovementAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl<S> knuffel::Decode<S> for WindowOpenAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().anim;
let mut custom_shader = None;
let anim = Animation::decode_node(node, ctx, default, |child, ctx| {
if &**child.node_name == "custom-shader" {
custom_shader = parse_arg_node("custom-shader", child, ctx)?;
Ok(true)
} else {
Ok(false)
}
})?;
Ok(Self {
anim,
custom_shader,
})
}
}
impl<S> knuffel::Decode<S> for WindowCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().anim;
let mut custom_shader = None;
let anim = Animation::decode_node(node, ctx, default, |child, ctx| {
if &**child.node_name == "custom-shader" {
custom_shader = parse_arg_node("custom-shader", child, ctx)?;
Ok(true)
} else {
Ok(false)
}
})?;
Ok(Self {
anim,
custom_shader,
})
}
}
impl<S> knuffel::Decode<S> for WindowResizeAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().anim;
let mut custom_shader = None;
let anim = Animation::decode_node(node, ctx, default, |child, ctx| {
if &**child.node_name == "custom-shader" {
custom_shader = parse_arg_node("custom-shader", child, ctx)?;
Ok(true)
} else {
Ok(false)
}
})?;
Ok(Self {
anim,
custom_shader,
})
}
}
impl<S> knuffel::Decode<S> for ConfigNotificationOpenCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl<S> knuffel::Decode<S> for ExitConfirmationOpenCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl<S> knuffel::Decode<S> for ScreenshotUiOpenAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl<S> knuffel::Decode<S> for OverviewOpenCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl<S> knuffel::Decode<S> for RecentWindowsCloseAnim
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
let default = Self::default().0;
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
Ok(false)
})?))
}
}
impl Animation {
pub fn new_off() -> Self {
Self {
off: true,
kind: Kind::Easing(EasingParams {
duration_ms: 0,
curve: Curve::Linear,
}),
}
}
fn decode_node<S: knuffel::traits::ErrorSpan>(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
default: Self,
mut process_children: impl FnMut(
&knuffel::ast::SpannedNode<S>,
&mut knuffel::decode::Context<S>,
) -> Result<bool, DecodeError<S>>,
) -> Result<Self, DecodeError<S>> {
#[derive(Default, PartialEq)]
struct OptionalEasingParams {
duration_ms: Option<u32>,
curve: Option<Curve>,
}
expect_only_children(node, ctx);
let mut off = false;
let mut easing_params = OptionalEasingParams::default();
let mut spring_params = None;
for child in node.children() {
match &**child.node_name {
"off" => {
knuffel::decode::check_flag_node(child, ctx);
if off {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `off`, single node expected",
));
} else {
off = true;
}
}
"spring" => {
if easing_params != OptionalEasingParams::default() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
"cannot set both spring and easing parameters at once",
));
}
if spring_params.is_some() {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `spring`, single node expected",
));
}
spring_params = Some(SpringParams::decode_node(child, ctx)?);
}
"duration-ms" => {
if spring_params.is_some() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
"cannot set both spring and easing parameters at once",
));
}
if easing_params.duration_ms.is_some() {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `duration-ms`, single node expected",
));
}
easing_params.duration_ms = Some(parse_arg_node("duration-ms", child, ctx)?);
}
"curve" => {
if spring_params.is_some() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
"cannot set both spring and easing parameters at once",
));
}
if easing_params.curve.is_some() {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `curve`, single node expected",
));
}
let mut iter_args = child.arguments.iter();
let val = iter_args.next().ok_or_else(|| {
DecodeError::missing(child, "additional argument `curve` is required")
})?;
let animation_curve_string: String =
knuffel::traits::DecodeScalar::decode(val, ctx)?;
let animation_curve = match animation_curve_string.as_str() {
"linear" => Some(Curve::Linear),
"ease-out-quad" => Some(Curve::EaseOutQuad),
"ease-out-cubic" => Some(Curve::EaseOutCubic),
"ease-out-expo" => Some(Curve::EaseOutExpo),
"cubic-bezier" => {
let val = iter_args.next().ok_or_else(|| {
DecodeError::missing(
child,
"missing x1 coordinate for cubic Bézier curve control point",
)
})?;
// the X axis represents time frame so it cannot be negative
// or larger than 1
let x1: FloatOrInt<0, 1> =
knuffel::traits::DecodeScalar::decode(val, ctx)?;
let val = iter_args.next().ok_or_else(|| {
DecodeError::missing(
child,
"missing y1 coordinate for cubic Bézier curve control point",
)
})?;
let y1: FloatOrInt<{ i32::MIN }, { i32::MAX }> =
knuffel::traits::DecodeScalar::decode(val, ctx)?;
let val = iter_args.next().ok_or_else(|| {
DecodeError::missing(
child,
"missing x2 coordinate for cubic Bézier curve control point",
)
})?;
let x2: FloatOrInt<0, 1> =
knuffel::traits::DecodeScalar::decode(val, ctx)?;
let val = iter_args.next().ok_or_else(|| {
DecodeError::missing(
child,
"missing y2 coordinate for cubic Bézier curve control point",
)
})?;
let y2: FloatOrInt<{ i32::MIN }, { i32::MAX }> =
knuffel::traits::DecodeScalar::decode(val, ctx)?;
Some(Curve::CubicBezier(x1.0, y1.0, x2.0, y2.0))
}
unexpected_curve => {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
format!(
"unexpected animation curve `{unexpected_curve}`. \
Niri only supports five animation curves: \
`ease-out-quad`, `ease-out-cubic`, `ease-out-expo`, `linear` and `cubic-bezier`."
),
));
None
}
};
if let Some(val) = iter_args.next() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"unexpected argument",
));
}
for name in child.properties.keys() {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name.escape_default()),
));
}
for child in child.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
easing_params.curve = animation_curve;
}
name_str => {
if !process_children(child, ctx)? {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", name_str.escape_default()),
));
}
}
}
}
let kind = if let Some(spring_params) = spring_params {
// Configured spring.
Kind::Spring(spring_params)
} else if easing_params == OptionalEasingParams::default() {
// Did not configure anything.
default.kind
} else {
// Configured easing.
let default = if let Kind::Easing(easing) = default.kind {
easing
} else {
// Generic fallback values for when the default animation is spring, but the user
// configured an easing animation.
EasingParams {
duration_ms: 250,
curve: Curve::EaseOutCubic,
}
};
Kind::Easing(EasingParams {
duration_ms: easing_params.duration_ms.unwrap_or(default.duration_ms),
curve: easing_params.curve.unwrap_or(default.curve),
})
};
Ok(Self { off, kind })
}
}
impl<S> knuffel::Decode<S> for SpringParams
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
if let Some(val) = node.arguments.first() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"unexpected argument",
));
}
for child in node.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
let mut damping_ratio = None;
let mut stiffness = None;
let mut epsilon = None;
for (name, val) in &node.properties {
match &***name {
"damping-ratio" => {
damping_ratio = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
}
"stiffness" => {
stiffness = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
}
"epsilon" => {
epsilon = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
}
name_str => {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name_str.escape_default()),
));
}
}
}
let damping_ratio = damping_ratio
.ok_or_else(|| DecodeError::missing(node, "property `damping-ratio` is required"))?;
let stiffness = stiffness
.ok_or_else(|| DecodeError::missing(node, "property `stiffness` is required"))?;
let epsilon =
epsilon.ok_or_else(|| DecodeError::missing(node, "property `epsilon` is required"))?;
if !(0.1..=10.).contains(&damping_ratio) {
ctx.emit_error(DecodeError::conversion(
node,
"damping-ratio must be between 0.1 and 10.0",
));
}
if stiffness < 1 {
ctx.emit_error(DecodeError::conversion(node, "stiffness must be >= 1"));
}
if !(0.00001..=0.1).contains(&epsilon) {
ctx.emit_error(DecodeError::conversion(
node,
"epsilon must be between 0.00001 and 0.1",
));
}
Ok(SpringParams {
damping_ratio,
stiffness,
epsilon,
})
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-111
View File
@@ -1,111 +0,0 @@
use std::path::PathBuf;
use crate::utils::{Flag, MergeWith};
#[derive(Debug, Default, PartialEq)]
pub struct Debug {
pub preview_render: Option<PreviewRender>,
pub dbus_interfaces_in_non_session_instances: bool,
pub wait_for_frame_completion_before_queueing: bool,
pub enable_overlay_planes: bool,
pub disable_cursor_plane: bool,
pub disable_direct_scanout: bool,
pub keep_max_bpc_unchanged: bool,
pub restrict_primary_scanout_to_matching_format: bool,
pub force_disable_connectors_on_resume: bool,
pub render_drm_device: Option<PathBuf>,
pub ignored_drm_devices: Vec<PathBuf>,
pub force_pipewire_invalid_modifier: bool,
pub emulate_zero_presentation_time: bool,
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
pub keep_laptop_panel_on_when_lid_is_closed: bool,
pub disable_monitor_names: bool,
pub strict_new_window_focus_policy: bool,
pub honor_xdg_activation_with_invalid_serial: bool,
pub deactivate_unfocused_windows: bool,
pub skip_cursor_only_updates_during_vrr: bool,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct DebugPart {
#[knuffel(child, unwrap(argument))]
pub preview_render: Option<PreviewRender>,
#[knuffel(child)]
pub dbus_interfaces_in_non_session_instances: Option<Flag>,
#[knuffel(child)]
pub wait_for_frame_completion_before_queueing: Option<Flag>,
#[knuffel(child)]
pub enable_overlay_planes: Option<Flag>,
#[knuffel(child)]
pub disable_cursor_plane: Option<Flag>,
#[knuffel(child)]
pub disable_direct_scanout: Option<Flag>,
#[knuffel(child)]
pub keep_max_bpc_unchanged: Option<Flag>,
#[knuffel(child)]
pub restrict_primary_scanout_to_matching_format: Option<Flag>,
#[knuffel(child)]
pub force_disable_connectors_on_resume: Option<Flag>,
#[knuffel(child, unwrap(argument))]
pub render_drm_device: Option<PathBuf>,
#[knuffel(children(name = "ignore-drm-device"), unwrap(argument))]
pub ignored_drm_devices: Vec<PathBuf>,
#[knuffel(child)]
pub force_pipewire_invalid_modifier: Option<Flag>,
#[knuffel(child)]
pub emulate_zero_presentation_time: Option<Flag>,
#[knuffel(child)]
pub disable_resize_throttling: Option<Flag>,
#[knuffel(child)]
pub disable_transactions: Option<Flag>,
#[knuffel(child)]
pub keep_laptop_panel_on_when_lid_is_closed: Option<Flag>,
#[knuffel(child)]
pub disable_monitor_names: Option<Flag>,
#[knuffel(child)]
pub strict_new_window_focus_policy: Option<Flag>,
#[knuffel(child)]
pub honor_xdg_activation_with_invalid_serial: Option<Flag>,
#[knuffel(child)]
pub deactivate_unfocused_windows: Option<Flag>,
#[knuffel(child)]
pub skip_cursor_only_updates_during_vrr: Option<Flag>,
}
impl MergeWith<DebugPart> for Debug {
fn merge_with(&mut self, part: &DebugPart) {
merge!(
(self, part),
dbus_interfaces_in_non_session_instances,
wait_for_frame_completion_before_queueing,
enable_overlay_planes,
disable_cursor_plane,
disable_direct_scanout,
keep_max_bpc_unchanged,
restrict_primary_scanout_to_matching_format,
force_disable_connectors_on_resume,
force_pipewire_invalid_modifier,
emulate_zero_presentation_time,
disable_resize_throttling,
disable_transactions,
keep_laptop_panel_on_when_lid_is_closed,
disable_monitor_names,
strict_new_window_focus_policy,
honor_xdg_activation_with_invalid_serial,
deactivate_unfocused_windows,
skip_cursor_only_updates_during_vrr,
);
merge_clone_opt!((self, part), preview_render, render_drm_device);
self.ignored_drm_devices
.extend(part.ignored_drm_devices.iter().cloned());
}
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreviewRender {
Screencast,
ScreenCapture,
}
-99
View File
@@ -1,99 +0,0 @@
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
use miette::Diagnostic;
#[derive(Debug)]
pub struct ConfigParseResult<T, E> {
pub config: Result<T, E>,
// We always try to return includes for the file watcher.
//
// If the main config is valid, but an included file fails to parse, config will be an Err(),
// but includes will still be filled, so that fixing just the included file is enough to
// trigger a reload.
pub includes: Vec<PathBuf>,
}
/// Error type that chains main errors with include errors.
///
/// Allows miette's Report formatting to have main + include errors all in one.
#[derive(Debug)]
pub struct ConfigIncludeError {
pub main: knuffel::Error,
pub includes: Vec<knuffel::Error>,
}
impl<T, E> ConfigParseResult<T, E> {
pub fn from_err(err: E) -> Self {
Self {
config: Err(err),
includes: Vec::new(),
}
}
pub fn map_config_res<U, V>(
self,
f: impl FnOnce(Result<T, E>) -> Result<U, V>,
) -> ConfigParseResult<U, V> {
ConfigParseResult {
config: f(self.config),
includes: self.includes,
}
}
}
impl fmt::Display for ConfigIncludeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.main, f)
}
}
impl Error for ConfigIncludeError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.main.source()
}
}
impl Diagnostic for ConfigIncludeError {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.main.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.main.severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.main.help()
}
fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.main.url()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.main.source_code()
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
self.main.labels()
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.main.diagnostic_source()
}
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
let main_related = self.main.related();
let includes_iter = self.includes.iter().map(|err| err as &'a dyn Diagnostic);
let iter: Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a> = match main_related {
Some(main) => Box::new(main.chain(includes_iter)),
None => Box::new(includes_iter),
};
Some(iter)
}
}
-112
View File
@@ -1,112 +0,0 @@
use crate::utils::MergeWith;
use crate::FloatOrInt;
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Gestures {
pub dnd_edge_view_scroll: DndEdgeViewScroll,
pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch,
pub hot_corners: HotCorners,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct GesturesPart {
#[knuffel(child)]
pub dnd_edge_view_scroll: Option<DndEdgeViewScrollPart>,
#[knuffel(child)]
pub dnd_edge_workspace_switch: Option<DndEdgeWorkspaceSwitchPart>,
#[knuffel(child)]
pub hot_corners: Option<HotCorners>,
}
impl MergeWith<GesturesPart> for Gestures {
fn merge_with(&mut self, part: &GesturesPart) {
merge!(
(self, part),
dnd_edge_view_scroll,
dnd_edge_workspace_switch,
);
merge_clone!((self, part), hot_corners);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DndEdgeViewScroll {
pub trigger_width: f64,
pub delay_ms: u16,
pub max_speed: f64,
}
impl Default for DndEdgeViewScroll {
fn default() -> Self {
Self {
trigger_width: 30., // Taken from GTK 4.
delay_ms: 100,
max_speed: 1500.,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct DndEdgeViewScrollPart {
#[knuffel(child, unwrap(argument))]
pub trigger_width: Option<FloatOrInt<0, 65535>>,
#[knuffel(child, unwrap(argument))]
pub delay_ms: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_speed: Option<FloatOrInt<0, 1_000_000>>,
}
impl MergeWith<DndEdgeViewScrollPart> for DndEdgeViewScroll {
fn merge_with(&mut self, part: &DndEdgeViewScrollPart) {
merge!((self, part), trigger_width, max_speed);
merge_clone!((self, part), delay_ms);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DndEdgeWorkspaceSwitch {
pub trigger_height: f64,
pub delay_ms: u16,
pub max_speed: f64,
}
impl Default for DndEdgeWorkspaceSwitch {
fn default() -> Self {
Self {
trigger_height: 50.,
delay_ms: 100,
max_speed: 1500.,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct DndEdgeWorkspaceSwitchPart {
#[knuffel(child, unwrap(argument))]
pub trigger_height: Option<FloatOrInt<0, 65535>>,
#[knuffel(child, unwrap(argument))]
pub delay_ms: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_speed: Option<FloatOrInt<0, 1_000_000>>,
}
impl MergeWith<DndEdgeWorkspaceSwitchPart> for DndEdgeWorkspaceSwitch {
fn merge_with(&mut self, part: &DndEdgeWorkspaceSwitchPart) {
merge!((self, part), trigger_height, max_speed);
merge_clone!((self, part), delay_ms);
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct HotCorners {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub top_left: bool,
#[knuffel(child)]
pub top_right: bool,
#[knuffel(child)]
pub bottom_left: bool,
#[knuffel(child)]
pub bottom_right: bool,
}
-747
View File
@@ -1,747 +0,0 @@
use std::str::FromStr;
use miette::miette;
use smithay::input::keyboard::XkbConfig;
use smithay::reexports::input;
use crate::binds::Modifiers;
use crate::utils::{Flag, MergeWith, Percent};
use crate::FloatOrInt;
#[derive(Debug, Default, PartialEq)]
pub struct Input {
pub keyboard: Keyboard,
pub touchpad: Touchpad,
pub mouse: Mouse,
pub trackpoint: Trackpoint,
pub trackball: Trackball,
pub tablet: Tablet,
pub touch: Touch,
pub disable_power_key_handling: bool,
pub warp_mouse_to_focus: Option<WarpMouseToFocus>,
pub focus_follows_mouse: Option<FocusFollowsMouse>,
pub workspace_auto_back_and_forth: bool,
pub mod_key: Option<ModKey>,
pub mod_key_nested: Option<ModKey>,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct InputPart {
#[knuffel(child)]
pub keyboard: Option<KeyboardPart>,
#[knuffel(child)]
pub touchpad: Option<Touchpad>,
#[knuffel(child)]
pub mouse: Option<Mouse>,
#[knuffel(child)]
pub trackpoint: Option<Trackpoint>,
#[knuffel(child)]
pub trackball: Option<Trackball>,
#[knuffel(child)]
pub tablet: Option<Tablet>,
#[knuffel(child)]
pub touch: Option<Touch>,
#[knuffel(child)]
pub disable_power_key_handling: Option<Flag>,
#[knuffel(child)]
pub warp_mouse_to_focus: Option<WarpMouseToFocus>,
#[knuffel(child)]
pub focus_follows_mouse: Option<FocusFollowsMouse>,
#[knuffel(child)]
pub workspace_auto_back_and_forth: Option<Flag>,
#[knuffel(child, unwrap(argument, str))]
pub mod_key: Option<ModKey>,
#[knuffel(child, unwrap(argument, str))]
pub mod_key_nested: Option<ModKey>,
}
impl MergeWith<InputPart> for Input {
fn merge_with(&mut self, part: &InputPart) {
merge!(
(self, part),
keyboard,
disable_power_key_handling,
workspace_auto_back_and_forth,
);
merge_clone!(
(self, part),
touchpad,
mouse,
trackpoint,
trackball,
tablet,
touch,
);
merge_clone_opt!(
(self, part),
warp_mouse_to_focus,
focus_follows_mouse,
mod_key,
mod_key_nested,
);
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Keyboard {
pub xkb: Xkb,
pub repeat_delay: u16,
pub repeat_rate: u8,
pub track_layout: TrackLayout,
pub numlock: bool,
}
impl Default for Keyboard {
fn default() -> Self {
Self {
xkb: Default::default(),
// The defaults were chosen to match wlroots and sway.
repeat_delay: 600,
repeat_rate: 25,
track_layout: Default::default(),
numlock: Default::default(),
}
}
}
#[derive(knuffel::Decode, Debug, PartialEq, Eq)]
pub struct KeyboardPart {
#[knuffel(child)]
pub xkb: Option<Xkb>,
#[knuffel(child, unwrap(argument))]
pub repeat_delay: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub repeat_rate: Option<u8>,
#[knuffel(child, unwrap(argument))]
pub track_layout: Option<TrackLayout>,
#[knuffel(child)]
pub numlock: Option<Flag>,
}
impl MergeWith<KeyboardPart> for Keyboard {
fn merge_with(&mut self, part: &KeyboardPart) {
merge_clone!((self, part), xkb, repeat_delay, repeat_rate, track_layout);
merge!((self, part), numlock);
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq, Clone)]
pub struct Xkb {
#[knuffel(child, unwrap(argument), default)]
pub rules: String,
#[knuffel(child, unwrap(argument), default)]
pub model: String,
#[knuffel(child, unwrap(argument), default)]
pub layout: String,
#[knuffel(child, unwrap(argument), default)]
pub variant: String,
#[knuffel(child, unwrap(argument))]
pub options: Option<String>,
#[knuffel(child, unwrap(argument))]
pub file: Option<String>,
}
impl Xkb {
pub fn to_xkb_config(&self) -> XkbConfig<'_> {
XkbConfig {
rules: &self.rules,
model: &self.model,
layout: &self.layout,
variant: &self.variant,
options: self.options.clone(),
}
}
}
#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TrackLayout {
/// The layout change is global.
#[default]
Global,
/// The layout change is window local.
Window,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct ScrollFactor {
#[knuffel(argument)]
pub base: Option<FloatOrInt<0, 100>>,
#[knuffel(property)]
pub horizontal: Option<FloatOrInt<-100, 100>>,
#[knuffel(property)]
pub vertical: Option<FloatOrInt<-100, 100>>,
}
impl ScrollFactor {
pub fn h_v_factors(&self) -> (f64, f64) {
let base_value = self.base.map(|f| f.0).unwrap_or(1.0);
let h = self.horizontal.map(|f| f.0).unwrap_or(base_value);
let v = self.vertical.map(|f| f.0).unwrap_or(base_value);
(h, v)
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Touchpad {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub tap: bool,
#[knuffel(child)]
pub dwt: bool,
#[knuffel(child)]
pub dwtp: bool,
#[knuffel(child, unwrap(argument))]
pub drag: Option<bool>,
#[knuffel(child)]
pub drag_lock: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument, str))]
pub click_method: Option<ClickMethod>,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: FloatOrInt<-1, 1>,
#[knuffel(child, unwrap(argument, str))]
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child)]
pub scroll_button_lock: bool,
#[knuffel(child, unwrap(argument, str))]
pub tap_button_map: Option<TapButtonMap>,
#[knuffel(child)]
pub left_handed: bool,
#[knuffel(child)]
pub disabled_on_external_mouse: bool,
#[knuffel(child)]
pub middle_emulation: bool,
#[knuffel(child)]
pub scroll_factor: Option<ScrollFactor>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Mouse {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: FloatOrInt<-1, 1>,
#[knuffel(child, unwrap(argument, str))]
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child)]
pub scroll_button_lock: bool,
#[knuffel(child)]
pub left_handed: bool,
#[knuffel(child)]
pub middle_emulation: bool,
#[knuffel(child)]
pub scroll_factor: Option<ScrollFactor>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Trackpoint {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: FloatOrInt<-1, 1>,
#[knuffel(child, unwrap(argument, str))]
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child)]
pub scroll_button_lock: bool,
#[knuffel(child)]
pub left_handed: bool,
#[knuffel(child)]
pub middle_emulation: bool,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Trackball {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: FloatOrInt<-1, 1>,
#[knuffel(child, unwrap(argument, str))]
pub accel_profile: Option<AccelProfile>,
#[knuffel(child, unwrap(argument, str))]
pub scroll_method: Option<ScrollMethod>,
#[knuffel(child, unwrap(argument))]
pub scroll_button: Option<u32>,
#[knuffel(child)]
pub scroll_button_lock: bool,
#[knuffel(child)]
pub left_handed: bool,
#[knuffel(child)]
pub middle_emulation: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClickMethod {
Clickfinger,
ButtonAreas,
}
impl From<ClickMethod> for input::ClickMethod {
fn from(value: ClickMethod) -> Self {
match value {
ClickMethod::Clickfinger => Self::Clickfinger,
ClickMethod::ButtonAreas => Self::ButtonAreas,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccelProfile {
Adaptive,
Flat,
}
impl From<AccelProfile> for input::AccelProfile {
fn from(value: AccelProfile) -> Self {
match value {
AccelProfile::Adaptive => Self::Adaptive,
AccelProfile::Flat => Self::Flat,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollMethod {
NoScroll,
TwoFinger,
Edge,
OnButtonDown,
}
impl From<ScrollMethod> for input::ScrollMethod {
fn from(value: ScrollMethod) -> Self {
match value {
ScrollMethod::NoScroll => Self::NoScroll,
ScrollMethod::TwoFinger => Self::TwoFinger,
ScrollMethod::Edge => Self::Edge,
ScrollMethod::OnButtonDown => Self::OnButtonDown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TapButtonMap {
LeftRightMiddle,
LeftMiddleRight,
}
impl From<TapButtonMap> for input::TapButtonMap {
fn from(value: TapButtonMap) -> Self {
match value {
TapButtonMap::LeftRightMiddle => Self::LeftRightMiddle,
TapButtonMap::LeftMiddleRight => Self::LeftMiddleRight,
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Tablet {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(arguments))]
pub calibration_matrix: Option<Vec<f32>>,
#[knuffel(child, unwrap(argument))]
pub map_to_output: Option<String>,
#[knuffel(child)]
pub map_to_focused_output: bool,
#[knuffel(child)]
pub left_handed: bool,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Touch {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(arguments))]
pub calibration_matrix: Option<Vec<f32>>,
#[knuffel(child, unwrap(argument))]
pub map_to_output: Option<String>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct FocusFollowsMouse {
#[knuffel(property, str)]
pub max_scroll_amount: Option<Percent>,
}
#[derive(knuffel::Decode, Debug, PartialEq, Eq, Clone, Copy)]
pub struct WarpMouseToFocus {
#[knuffel(property, str)]
pub mode: Option<WarpMouseToFocusMode>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum WarpMouseToFocusMode {
CenterXy,
CenterXyAlways,
}
impl FromStr for WarpMouseToFocusMode {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"center-xy" => Ok(Self::CenterXy),
"center-xy-always" => Ok(Self::CenterXyAlways),
_ => Err(miette!(
r#"invalid mode for warp-mouse-to-focus, can be "center-xy" or "center-xy-always" (or leave unset for separate centering)"#
)),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ModKey {
Ctrl,
Shift,
Alt,
Super,
IsoLevel3Shift,
IsoLevel5Shift,
}
impl ModKey {
pub fn to_modifiers(&self) -> Modifiers {
match self {
ModKey::Ctrl => Modifiers::CTRL,
ModKey::Shift => Modifiers::SHIFT,
ModKey::Alt => Modifiers::ALT,
ModKey::Super => Modifiers::SUPER,
ModKey::IsoLevel3Shift => Modifiers::ISO_LEVEL3_SHIFT,
ModKey::IsoLevel5Shift => Modifiers::ISO_LEVEL5_SHIFT,
}
}
}
impl FromStr for ModKey {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match &*s.to_ascii_lowercase() {
"ctrl" | "control" => Ok(Self::Ctrl),
"shift" => Ok(Self::Shift),
"alt" => Ok(Self::Alt),
"super" | "win" => Ok(Self::Super),
"iso_level3_shift" | "mod5" => Ok(Self::IsoLevel3Shift),
"iso_level5_shift" | "mod3" => Ok(Self::IsoLevel5Shift),
_ => Err(miette!("invalid Mod key: {s}")),
}
}
}
impl FromStr for ClickMethod {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"clickfinger" => Ok(Self::Clickfinger),
"button-areas" => Ok(Self::ButtonAreas),
_ => Err(miette!(
r#"invalid click method, can be "button-areas" or "clickfinger""#
)),
}
}
}
impl FromStr for AccelProfile {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"adaptive" => Ok(Self::Adaptive),
"flat" => Ok(Self::Flat),
_ => Err(miette!(
r#"invalid accel profile, can be "adaptive" or "flat""#
)),
}
}
}
impl FromStr for ScrollMethod {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"no-scroll" => Ok(Self::NoScroll),
"two-finger" => Ok(Self::TwoFinger),
"edge" => Ok(Self::Edge),
"on-button-down" => Ok(Self::OnButtonDown),
_ => Err(miette!(
r#"invalid scroll method, can be "no-scroll", "two-finger", "edge", or "on-button-down""#
)),
}
}
}
impl FromStr for TapButtonMap {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left-right-middle" => Ok(Self::LeftRightMiddle),
"left-middle-right" => Ok(Self::LeftMiddleRight),
_ => Err(miette!(
r#"invalid tap button map, can be "left-right-middle" or "left-middle-right""#
)),
}
}
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use super::*;
#[track_caller]
fn do_parse(text: &str) -> Input {
let part = knuffel::parse("test.kdl", text)
.map_err(miette::Report::new)
.unwrap();
Input::from_part(&part)
}
#[test]
fn parse_scroll_factor_combined() {
// Test combined scroll-factor syntax
let parsed = do_parse(
r#"
mouse {
scroll-factor 2.0
}
touchpad {
scroll-factor 1.5
}
"#,
);
assert_debug_snapshot!(parsed.mouse.scroll_factor, @r#"
Some(
ScrollFactor {
base: Some(
FloatOrInt(
2.0,
),
),
horizontal: None,
vertical: None,
},
)
"#);
assert_debug_snapshot!(parsed.touchpad.scroll_factor, @r#"
Some(
ScrollFactor {
base: Some(
FloatOrInt(
1.5,
),
),
horizontal: None,
vertical: None,
},
)
"#);
}
#[test]
fn parse_scroll_factor_split() {
// Test split horizontal/vertical syntax
let parsed = do_parse(
r#"
mouse {
scroll-factor horizontal=2.0 vertical=-1.0
}
touchpad {
scroll-factor horizontal=-1.5 vertical=0.5
}
"#,
);
assert_debug_snapshot!(parsed.mouse.scroll_factor, @r#"
Some(
ScrollFactor {
base: None,
horizontal: Some(
FloatOrInt(
2.0,
),
),
vertical: Some(
FloatOrInt(
-1.0,
),
),
},
)
"#);
assert_debug_snapshot!(parsed.touchpad.scroll_factor, @r#"
Some(
ScrollFactor {
base: None,
horizontal: Some(
FloatOrInt(
-1.5,
),
),
vertical: Some(
FloatOrInt(
0.5,
),
),
},
)
"#);
}
#[test]
fn parse_scroll_factor_partial() {
// Test partial specification (only one axis)
let parsed = do_parse(
r#"
mouse {
scroll-factor horizontal=2.0
}
touchpad {
scroll-factor vertical=-1.5
}
"#,
);
assert_debug_snapshot!(parsed.mouse.scroll_factor, @r#"
Some(
ScrollFactor {
base: None,
horizontal: Some(
FloatOrInt(
2.0,
),
),
vertical: None,
},
)
"#);
assert_debug_snapshot!(parsed.touchpad.scroll_factor, @r#"
Some(
ScrollFactor {
base: None,
horizontal: None,
vertical: Some(
FloatOrInt(
-1.5,
),
),
},
)
"#);
}
#[test]
fn parse_scroll_factor_mixed() {
// Test mixed base + override syntax
let parsed = do_parse(
r#"
mouse {
scroll-factor 2 vertical=-1
}
touchpad {
scroll-factor 1.5 horizontal=3
}
"#,
);
assert_debug_snapshot!(parsed.mouse.scroll_factor, @r#"
Some(
ScrollFactor {
base: Some(
FloatOrInt(
2.0,
),
),
horizontal: None,
vertical: Some(
FloatOrInt(
-1.0,
),
),
},
)
"#);
assert_debug_snapshot!(parsed.touchpad.scroll_factor, @r#"
Some(
ScrollFactor {
base: Some(
FloatOrInt(
1.5,
),
),
horizontal: Some(
FloatOrInt(
3.0,
),
),
vertical: None,
},
)
"#);
}
#[test]
fn scroll_factor_h_v_factors() {
let sf = ScrollFactor {
base: Some(FloatOrInt(2.0)),
horizontal: None,
vertical: None,
};
assert_debug_snapshot!(sf.h_v_factors(), @r#"
(
2.0,
2.0,
)
"#);
let sf = ScrollFactor {
base: None,
horizontal: Some(FloatOrInt(3.0)),
vertical: Some(FloatOrInt(-1.0)),
};
assert_debug_snapshot!(sf.h_v_factors(), @r#"
(
3.0,
-1.0,
)
"#);
let sf = ScrollFactor {
base: Some(FloatOrInt(2.0)),
horizontal: Some(FloatOrInt(1.0)),
vertical: None,
};
assert_debug_snapshot!(sf.h_v_factors(), @r"
(
1.0,
2.0,
)
");
}
}
+1 -13
View File
@@ -1,6 +1,4 @@
use crate::appearance::{BackgroundEffectRule, BlockOutFrom, CornerRadius, ShadowRule};
use crate::utils::RegexEq;
use crate::window_rule::PopupsRule;
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
@@ -17,14 +15,6 @@ pub struct LayerRule {
pub shadow: ShadowRule,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, unwrap(argument))]
pub place_within_backdrop: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
#[knuffel(child, default)]
pub popups: PopupsRule,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -33,6 +23,4 @@ pub struct Match {
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
#[knuffel(property, str)]
pub layer: Option<niri_ipc::Layer>,
}
-198
View File
@@ -1,198 +0,0 @@
use knuffel::errors::DecodeError;
use niri_ipc::{ColumnDisplay, SizeChange};
use crate::appearance::{
Border, FocusRing, InsertHint, Shadow, TabIndicator, DEFAULT_BACKGROUND_COLOR,
};
use crate::utils::{expect_only_children, Flag, MergeWith};
use crate::{BorderRule, Color, FloatOrInt, InsertHintPart, ShadowRule, TabIndicatorPart};
#[derive(Debug, Clone, PartialEq)]
pub struct Layout {
pub focus_ring: FocusRing,
pub border: Border,
pub shadow: Shadow,
pub tab_indicator: TabIndicator,
pub insert_hint: InsertHint,
pub preset_column_widths: Vec<PresetSize>,
pub default_column_width: Option<PresetSize>,
pub preset_window_heights: Vec<PresetSize>,
pub center_focused_column: CenterFocusedColumn,
pub always_center_single_column: bool,
pub empty_workspace_above_first: bool,
pub default_column_display: ColumnDisplay,
pub gaps: f64,
pub struts: Struts,
pub background_color: Color,
}
impl Default for Layout {
fn default() -> Self {
Self {
focus_ring: FocusRing::default(),
border: Border::default(),
shadow: Shadow::default(),
tab_indicator: TabIndicator::default(),
insert_hint: InsertHint::default(),
preset_column_widths: vec![
PresetSize::Proportion(1. / 3.),
PresetSize::Proportion(0.5),
PresetSize::Proportion(2. / 3.),
],
default_column_width: Some(PresetSize::Proportion(0.5)),
center_focused_column: CenterFocusedColumn::Never,
always_center_single_column: false,
empty_workspace_above_first: false,
default_column_display: ColumnDisplay::Normal,
gaps: 16.,
struts: Struts::default(),
preset_window_heights: vec![
PresetSize::Proportion(1. / 3.),
PresetSize::Proportion(0.5),
PresetSize::Proportion(2. / 3.),
],
background_color: DEFAULT_BACKGROUND_COLOR,
}
}
}
impl MergeWith<LayoutPart> for Layout {
fn merge_with(&mut self, part: &LayoutPart) {
merge!(
(self, part),
focus_ring,
border,
shadow,
tab_indicator,
insert_hint,
always_center_single_column,
empty_workspace_above_first,
gaps,
);
merge_clone!(
(self, part),
preset_column_widths,
preset_window_heights,
center_focused_column,
default_column_display,
struts,
background_color,
);
if let Some(x) = part.default_column_width {
self.default_column_width = x.0;
}
if self.preset_column_widths.is_empty() {
self.preset_column_widths = Layout::default().preset_column_widths;
}
if self.preset_window_heights.is_empty() {
self.preset_window_heights = Layout::default().preset_window_heights;
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayoutPart {
#[knuffel(child)]
pub focus_ring: Option<BorderRule>,
#[knuffel(child)]
pub border: Option<BorderRule>,
#[knuffel(child)]
pub shadow: Option<ShadowRule>,
#[knuffel(child)]
pub tab_indicator: Option<TabIndicatorPart>,
#[knuffel(child)]
pub insert_hint: Option<InsertHintPart>,
#[knuffel(child, unwrap(children))]
pub preset_column_widths: Option<Vec<PresetSize>>,
#[knuffel(child)]
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(children))]
pub preset_window_heights: Option<Vec<PresetSize>>,
#[knuffel(child, unwrap(argument))]
pub center_focused_column: Option<CenterFocusedColumn>,
#[knuffel(child)]
pub always_center_single_column: Option<Flag>,
#[knuffel(child)]
pub empty_workspace_above_first: Option<Flag>,
#[knuffel(child, unwrap(argument, str))]
pub default_column_display: Option<ColumnDisplay>,
#[knuffel(child, unwrap(argument))]
pub gaps: Option<FloatOrInt<0, 65535>>,
#[knuffel(child)]
pub struts: Option<Struts>,
#[knuffel(child)]
pub background_color: Option<Color>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub enum PresetSize {
Proportion(#[knuffel(argument)] f64),
Fixed(#[knuffel(argument)] i32),
}
impl From<PresetSize> for SizeChange {
fn from(value: PresetSize) -> Self {
match value {
PresetSize::Proportion(prop) => SizeChange::SetProportion(prop * 100.),
PresetSize::Fixed(fixed) => SizeChange::SetFixed(fixed),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DefaultPresetSize(pub Option<PresetSize>);
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct Struts {
#[knuffel(child, unwrap(argument), default)]
pub left: FloatOrInt<-65535, 65535>,
#[knuffel(child, unwrap(argument), default)]
pub right: FloatOrInt<-65535, 65535>,
#[knuffel(child, unwrap(argument), default)]
pub top: FloatOrInt<-65535, 65535>,
#[knuffel(child, unwrap(argument), default)]
pub bottom: FloatOrInt<-65535, 65535>,
}
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum CenterFocusedColumn {
/// Focusing a column will not center the column.
#[default]
Never,
/// The focused column will always be centered.
Always,
/// Focusing a column will center it if it doesn't fit on the screen together with the
/// previously focused column.
OnOverflow,
}
impl<S> knuffel::Decode<S> for DefaultPresetSize
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
expect_only_children(node, ctx);
let mut children = node.children();
if let Some(child) = children.next() {
if let Some(unwanted_child) = children.next() {
ctx.emit_error(DecodeError::unexpected(
unwanted_child,
"node",
"expected no more than one child",
));
}
PresetSize::decode_node(child, ctx).map(Some).map(Self)
} else {
Ok(Self(None))
}
}
}
+4052 -1067
View File
File diff suppressed because it is too large Load Diff
-71
View File
@@ -1,71 +0,0 @@
macro_rules! merge {
(($self:expr, $part:expr), $($field:ident),+ $(,)*) => {
$(
if let Some(x) = &$part.$field {
$self.$field.merge_with(x);
}
)+
};
}
macro_rules! merge_clone {
(($self:expr, $part:expr), $($field:ident),+ $(,)*) => {
$(
if let Some(x) = &$part.$field {
$self.$field.clone_from(x);
}
)+
};
}
macro_rules! merge_clone_opt {
(($self:expr, $part:expr), $($field:ident),+ $(,)*) => {
$(
if $part.$field.is_some() {
$self.$field.clone_from(&$part.$field);
}
)+
};
}
macro_rules! merge_color_gradient {
(($self:expr, $part:expr), $(($color:ident, $gradient:ident)),+ $(,)*) => {
$(
if let Some(x) = $part.$color {
$self.$color = x;
$self.$gradient = None;
}
if let Some(x) = $part.$gradient {
$self.$gradient = Some(x);
}
)+
};
}
macro_rules! merge_color_gradient_opt {
(($self:expr, $part:expr), $(($color:ident, $gradient:ident)),+ $(,)*) => {
$(
if let Some(x) = $part.$color {
$self.$color = Some(x);
$self.$gradient = None;
}
if let Some(x) = $part.$gradient {
$self.$gradient = Some(x);
}
)+
};
}
macro_rules! merge_on_off {
(($self:expr, $part:expr)) => {
if $part.off {
$self.off = true;
$self.on = false;
}
if $part.on {
$self.off = false;
$self.on = true;
}
};
}
-200
View File
@@ -1,200 +0,0 @@
use crate::appearance::{Color, WorkspaceShadow, WorkspaceShadowPart, DEFAULT_BACKDROP_COLOR};
use crate::utils::{Flag, MergeWith};
use crate::FloatOrInt;
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct SpawnAtStartup {
#[knuffel(arguments)]
pub command: Vec<String>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct SpawnShAtStartup {
#[knuffel(argument)]
pub command: String,
}
#[derive(Debug, PartialEq)]
pub struct Cursor {
pub xcursor_theme: String,
pub xcursor_size: u8,
pub hide_when_typing: bool,
pub hide_after_inactive_ms: Option<u32>,
}
impl Default for Cursor {
fn default() -> Self {
Self {
xcursor_theme: String::from("default"),
xcursor_size: 24,
hide_when_typing: false,
hide_after_inactive_ms: None,
}
}
}
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct CursorPart {
#[knuffel(child, unwrap(argument))]
pub xcursor_theme: Option<String>,
#[knuffel(child, unwrap(argument))]
pub xcursor_size: Option<u8>,
#[knuffel(child)]
pub hide_when_typing: Option<Flag>,
#[knuffel(child, unwrap(argument))]
pub hide_after_inactive_ms: Option<u32>,
}
impl MergeWith<CursorPart> for Cursor {
fn merge_with(&mut self, part: &CursorPart) {
merge_clone!((self, part), xcursor_theme, xcursor_size);
merge!((self, part), hide_when_typing);
merge_clone_opt!((self, part), hide_after_inactive_ms);
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct ScreenshotPath(#[knuffel(argument)] pub Option<String>);
impl Default for ScreenshotPath {
fn default() -> Self {
Self(Some(String::from(
"~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png",
)))
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct HotkeyOverlay {
pub skip_at_startup: bool,
pub hide_not_bound: bool,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct HotkeyOverlayPart {
#[knuffel(child)]
pub skip_at_startup: Option<Flag>,
#[knuffel(child)]
pub hide_not_bound: Option<Flag>,
}
impl MergeWith<HotkeyOverlayPart> for HotkeyOverlay {
fn merge_with(&mut self, part: &HotkeyOverlayPart) {
merge!((self, part), skip_at_startup, hide_not_bound);
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ConfigNotification {
pub disable_failed: bool,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ConfigNotificationPart {
#[knuffel(child)]
pub disable_failed: Option<Flag>,
}
impl MergeWith<ConfigNotificationPart> for ConfigNotification {
fn merge_with(&mut self, part: &ConfigNotificationPart) {
merge!((self, part), disable_failed);
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Clipboard {
pub disable_primary: bool,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ClipboardPart {
#[knuffel(child)]
pub disable_primary: Option<Flag>,
}
impl MergeWith<ClipboardPart> for Clipboard {
fn merge_with(&mut self, part: &ClipboardPart) {
merge!((self, part), disable_primary);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Overview {
pub zoom: f64,
pub backdrop_color: Color,
pub workspace_shadow: WorkspaceShadow,
}
impl Default for Overview {
fn default() -> Self {
Self {
zoom: 0.5,
backdrop_color: DEFAULT_BACKDROP_COLOR,
workspace_shadow: WorkspaceShadow::default(),
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct OverviewPart {
#[knuffel(child, unwrap(argument))]
pub zoom: Option<FloatOrInt<0, 1>>,
#[knuffel(child)]
pub backdrop_color: Option<Color>,
#[knuffel(child)]
pub workspace_shadow: Option<WorkspaceShadowPart>,
}
impl MergeWith<OverviewPart> for Overview {
fn merge_with(&mut self, part: &OverviewPart) {
merge!((self, part), zoom, workspace_shadow);
merge_clone!((self, part), backdrop_color);
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)]
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct EnvironmentVariable {
#[knuffel(node_name)]
pub name: String,
#[knuffel(argument)]
pub value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XwaylandSatellite {
pub off: bool,
pub path: String,
}
impl Default for XwaylandSatellite {
fn default() -> Self {
Self {
off: false,
path: String::from("xwayland-satellite"),
}
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct XwaylandSatellitePart {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub on: bool,
#[knuffel(child, unwrap(argument))]
pub path: Option<String>,
}
impl MergeWith<XwaylandSatellitePart> for XwaylandSatellite {
fn merge_with(&mut self, part: &XwaylandSatellitePart) {
self.off |= part.off;
if part.on {
self.off = false;
}
merge_clone!((self, part), path);
}
}
-646
View File
@@ -1,646 +0,0 @@
use std::str::FromStr;
use knuffel::ast::SpannedNode;
use knuffel::decode::Context;
use knuffel::errors::DecodeError;
use knuffel::traits::ErrorSpan;
use knuffel::Decode;
use niri_ipc::{ConfiguredMode, HSyncPolarity, Transform, VSyncPolarity};
use crate::gestures::HotCorners;
use crate::{Color, FloatOrInt, LayoutPart};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Outputs(pub Vec<Output>);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mode {
pub custom: bool,
pub mode: ConfiguredMode,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Modeline {
/// The rate at which pixels are drawn in MHz.
pub clock: f64,
/// Horizontal active pixels.
pub hdisplay: u16,
/// Horizontal sync pulse start position in pixels.
pub hsync_start: u16,
/// Horizontal sync pulse end position in pixels.
pub hsync_end: u16,
/// Total horizontal number of pixels before resetting the horizontal drawing position to
/// zero.
pub htotal: u16,
/// Vertical active pixels.
pub vdisplay: u16,
/// Vertical sync pulse start position in pixels.
pub vsync_start: u16,
/// Vertical sync pulse end position in pixels.
pub vsync_end: u16,
/// Total vertical number of pixels before resetting the vertical drawing position to zero.
pub vtotal: u16,
/// Horizontal sync polarity: "+hsync" or "-hsync".
pub hsync_polarity: niri_ipc::HSyncPolarity,
/// Vertical sync polarity: "+vsync" or "-vsync".
pub vsync_polarity: niri_ipc::VSyncPolarity,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Output {
#[knuffel(child)]
pub off: bool,
#[knuffel(argument)]
pub name: String,
#[knuffel(child, unwrap(argument))]
pub scale: Option<FloatOrInt<0, 10>>,
#[knuffel(child, unwrap(argument, str), default = Transform::Normal)]
pub transform: Transform,
#[knuffel(child)]
pub position: Option<Position>,
#[knuffel(child)]
pub mode: Option<Mode>,
#[knuffel(child)]
pub modeline: Option<Modeline>,
#[knuffel(child)]
pub variable_refresh_rate: Option<Vrr>,
#[knuffel(child)]
pub focus_at_startup: bool,
// Deprecated; use layout.background_color.
#[knuffel(child)]
pub background_color: Option<Color>,
#[knuffel(child)]
pub backdrop_color: Option<Color>,
#[knuffel(child)]
pub hot_corners: Option<HotCorners>,
#[knuffel(child)]
pub layout: Option<LayoutPart>,
}
impl Output {
pub fn is_vrr_always_on(&self) -> bool {
self.variable_refresh_rate == Some(Vrr { on_demand: false })
}
pub fn is_vrr_on_demand(&self) -> bool {
self.variable_refresh_rate == Some(Vrr { on_demand: true })
}
pub fn is_vrr_always_off(&self) -> bool {
self.variable_refresh_rate.is_none()
}
}
impl Default for Output {
fn default() -> Self {
Self {
off: false,
focus_at_startup: false,
name: String::new(),
scale: None,
transform: Transform::Normal,
position: None,
mode: None,
modeline: None,
variable_refresh_rate: None,
background_color: None,
backdrop_color: None,
hot_corners: None,
layout: None,
}
}
}
#[derive(Debug, Clone)]
pub struct OutputName {
pub connector: String,
pub make: Option<String>,
pub model: Option<String>,
pub serial: Option<String>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
#[knuffel(property)]
pub x: i32,
#[knuffel(property)]
pub y: i32,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Default)]
pub struct Vrr {
#[knuffel(property, default = false)]
pub on_demand: bool,
}
impl FromIterator<Output> for Outputs {
fn from_iter<T: IntoIterator<Item = Output>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl Outputs {
pub fn find(&self, name: &OutputName) -> Option<&Output> {
self.0.iter().find(|o| name.matches(&o.name))
}
pub fn find_mut(&mut self, name: &OutputName) -> Option<&mut Output> {
self.0.iter_mut().find(|o| name.matches(&o.name))
}
}
impl OutputName {
pub fn from_ipc_output(output: &niri_ipc::Output) -> Self {
Self {
connector: output.name.clone(),
make: (output.make != "Unknown").then(|| output.make.clone()),
model: (output.model != "Unknown").then(|| output.model.clone()),
serial: output.serial.clone(),
}
}
/// Returns an output description matching what Smithay's `Output::new()` does.
pub fn format_description(&self) -> String {
format!(
"{} - {} - {}",
self.make.as_deref().unwrap_or("Unknown"),
self.model.as_deref().unwrap_or("Unknown"),
self.connector,
)
}
/// Returns an output name that will match by make/model/serial or, if they are missing, by
/// connector.
pub fn format_make_model_serial_or_connector(&self) -> String {
if self.make.is_none() && self.model.is_none() && self.serial.is_none() {
self.connector.to_string()
} else {
self.format_make_model_serial()
}
}
pub fn format_make_model_serial(&self) -> String {
let make = self.make.as_deref().unwrap_or("Unknown");
let model = self.model.as_deref().unwrap_or("Unknown");
let serial = self.serial.as_deref().unwrap_or("Unknown");
format!("{make} {model} {serial}")
}
pub fn matches(&self, target: &str) -> bool {
// Match by connector.
if target.eq_ignore_ascii_case(&self.connector) {
return true;
}
// If no other fields are available, don't try to match by them.
//
// This is used by niri msg output.
if self.make.is_none() && self.model.is_none() && self.serial.is_none() {
return false;
}
// Match by "make model serial" with Unknown if something is missing.
let make = self.make.as_deref().unwrap_or("Unknown");
let model = self.model.as_deref().unwrap_or("Unknown");
let serial = self.serial.as_deref().unwrap_or("Unknown");
let Some(target_make) = target.get(..make.len()) else {
return false;
};
let rest = &target[make.len()..];
if !target_make.eq_ignore_ascii_case(make) {
return false;
}
if !rest.starts_with(' ') {
return false;
}
let rest = &rest[1..];
let Some(target_model) = rest.get(..model.len()) else {
return false;
};
let rest = &rest[model.len()..];
if !target_model.eq_ignore_ascii_case(model) {
return false;
}
if !rest.starts_with(' ') {
return false;
}
let rest = &rest[1..];
if !rest.eq_ignore_ascii_case(serial) {
return false;
}
true
}
// Similar in spirit to Ord, but I don't want to derive Eq to avoid mistakes (you should use
// `Self::match`, not Eq).
pub fn compare(&self, other: &Self) -> std::cmp::Ordering {
let self_missing_mms = self.make.is_none() && self.model.is_none() && self.serial.is_none();
let other_missing_mms =
other.make.is_none() && other.model.is_none() && other.serial.is_none();
match (self_missing_mms, other_missing_mms) {
(true, true) => self.connector.cmp(&other.connector),
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
(false, false) => self
.make
.cmp(&other.make)
.then_with(|| self.model.cmp(&other.model))
.then_with(|| self.serial.cmp(&other.serial))
.then_with(|| self.connector.cmp(&other.connector)),
}
}
}
impl<S: ErrorSpan> knuffel::Decode<S> for Mode {
fn decode_node(node: &SpannedNode<S>, ctx: &mut Context<S>) -> Result<Self, DecodeError<S>> {
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
for child in node.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
let mut custom: Option<bool> = None;
for (name, val) in &node.properties {
match &***name {
"custom" => {
if custom.is_some() {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
"unexpected duplicate property `custom`",
))
}
custom = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?)
}
name_str => ctx.emit_error(DecodeError::unexpected(
node,
"property",
format!("unexpected property `{}`", name_str.escape_default()),
)),
}
}
let custom = custom.unwrap_or(false);
let mut arguments = node.arguments.iter();
let mode = if let Some(mode_str) = arguments.next() {
let temp_mode: String = knuffel::traits::DecodeScalar::decode(mode_str, ctx)?;
let res = ConfiguredMode::from_str(temp_mode.as_str()).and_then(|mode| {
if custom {
if mode.refresh.is_none() {
return Err("no refresh rate found; required for custom mode");
} else if let Some(refresh) = mode.refresh {
if refresh <= 0. {
return Err("custom mode refresh rate must be > 0");
}
}
}
Ok(mode)
});
res.map_err(|err_msg| DecodeError::conversion(&mode_str.literal, err_msg))?
} else {
return Err(DecodeError::missing(node, "argument `mode` is required"));
};
if let Some(surplus) = arguments.next() {
ctx.emit_error(DecodeError::unexpected(
&surplus.literal,
"argument",
"unexpected argument",
))
}
Ok(Mode { custom, mode })
}
}
macro_rules! ensure {
($cond:expr, $ctx:expr, $span:expr, $fmt:literal $($arg:tt)* ) => {
if !$cond {
$ctx.emit_error(DecodeError::Conversion {
source: format!($fmt $($arg)*).into(),
span: $span.literal.span().clone()
});
}
};
}
impl<S: ErrorSpan> Decode<S> for Modeline {
fn decode_node(node: &SpannedNode<S>, ctx: &mut Context<S>) -> Result<Self, DecodeError<S>> {
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
for child in node.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
for span in node.properties.keys() {
ctx.emit_error(DecodeError::unexpected(
span,
"node",
format!("unexpected node `{}`", span.escape_default()),
));
}
let mut arguments = node.arguments.iter();
macro_rules! m_required {
// This could be one identifier if macro_metavar_expr_concat stabilizes
($field:ident, $value_field:ident) => {
let $value_field = arguments.next().ok_or_else(|| {
DecodeError::missing(node, format!("missing {} argument", stringify!($value)))
})?;
let $field = knuffel::traits::DecodeScalar::decode($value_field, ctx)?;
};
}
m_required!(clock, clock_value);
m_required!(hdisplay, hdisplay_value);
m_required!(hsync_start, hsync_start_value);
m_required!(hsync_end, hsync_end_value);
m_required!(htotal, htotal_value);
m_required!(vdisplay, vdisplay_value);
m_required!(vsync_start, vsync_start_value);
m_required!(vsync_end, vsync_end_value);
m_required!(vtotal, vtotal_value);
m_required!(hsync_polarity, hsync_polarity_value);
let hsync_polarity =
HSyncPolarity::from_str(String::as_str(&hsync_polarity)).map_err(|msg| {
DecodeError::Conversion {
span: hsync_polarity_value.literal.span().clone(),
source: msg.into(),
}
})?;
m_required!(vsync_polarity, vsync_polarity_value);
let vsync_polarity =
VSyncPolarity::from_str(String::as_str(&vsync_polarity)).map_err(|msg| {
DecodeError::Conversion {
span: vsync_polarity_value.literal.span().clone(),
source: msg.into(),
}
})?;
ensure!(
hdisplay < hsync_start,
ctx,
hdisplay_value,
"hdisplay {} must be < hsync_start {}",
hdisplay,
hsync_start
);
ensure!(
hsync_start < hsync_end,
ctx,
hsync_start_value,
"hsync_start {} must be < hsync_end {}",
hsync_start,
hsync_end,
);
ensure!(
hsync_end < htotal,
ctx,
hsync_end_value,
"hsync_end {} must be < htotal {}",
hsync_end,
htotal,
);
ensure!(
0u16 < htotal,
ctx,
htotal_value,
"htotal {} must be > 0",
htotal
);
ensure!(
vdisplay < vsync_start,
ctx,
vdisplay_value,
"vdisplay {} must be < vsync_start {}",
vdisplay,
vsync_start,
);
ensure!(
vsync_start < vsync_end,
ctx,
vsync_start_value,
"vsync_start {} must be < vsync_end {}",
vsync_start,
vsync_end,
);
ensure!(
vsync_end < vtotal,
ctx,
vsync_end_value,
"vsync_end {} must be < vtotal {}",
vsync_end,
vtotal,
);
ensure!(
0u16 < vtotal,
ctx,
vtotal_value,
"vtotal {} must be > 0",
vtotal
);
if let Some(extra) = arguments.next() {
ctx.emit_error(DecodeError::unexpected(
&extra.literal,
"argument",
"unexpected argument, all possible arguments were already provided",
))
}
Ok(Modeline {
clock,
hdisplay,
hsync_start,
hsync_end,
htotal,
vdisplay,
vsync_start,
vsync_end,
vtotal,
hsync_polarity,
vsync_polarity,
})
}
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use super::*;
#[test]
fn parse_mode() {
assert_eq!(
"2560x1600@165.004".parse::<ConfiguredMode>().unwrap(),
ConfiguredMode {
width: 2560,
height: 1600,
refresh: Some(165.004),
},
);
assert_eq!(
"1920x1080".parse::<ConfiguredMode>().unwrap(),
ConfiguredMode {
width: 1920,
height: 1080,
refresh: None,
},
);
assert!("1920".parse::<ConfiguredMode>().is_err());
assert!("1920x".parse::<ConfiguredMode>().is_err());
assert!("1920x1080@".parse::<ConfiguredMode>().is_err());
assert!("1920x1080@60Hz".parse::<ConfiguredMode>().is_err());
}
fn make_output_name(
connector: &str,
make: Option<&str>,
model: Option<&str>,
serial: Option<&str>,
) -> OutputName {
OutputName {
connector: connector.to_string(),
make: make.map(|x| x.to_string()),
model: model.map(|x| x.to_string()),
serial: serial.map(|x| x.to_string()),
}
}
#[test]
fn test_output_name_match() {
fn check(
target: &str,
connector: &str,
make: Option<&str>,
model: Option<&str>,
serial: Option<&str>,
) -> bool {
let name = make_output_name(connector, make, model, serial);
name.matches(target)
}
assert!(check("dp-2", "DP-2", None, None, None));
assert!(!check("dp-1", "DP-2", None, None, None));
assert!(check("dp-2", "DP-2", Some("a"), Some("b"), Some("c")));
assert!(check(
"some company some monitor 1234",
"DP-2",
Some("Some Company"),
Some("Some Monitor"),
Some("1234")
));
assert!(!check(
"some other company some monitor 1234",
"DP-2",
Some("Some Company"),
Some("Some Monitor"),
Some("1234")
));
assert!(!check(
"make model serial ",
"DP-2",
Some("make"),
Some("model"),
Some("serial")
));
assert!(check(
"make serial",
"DP-2",
Some("make"),
Some(""),
Some("serial")
));
assert!(check(
"make model unknown",
"DP-2",
Some("Make"),
Some("Model"),
None
));
assert!(check(
"unknown unknown serial",
"DP-2",
None,
None,
Some("Serial")
));
assert!(!check("unknown unknown unknown", "DP-2", None, None, None));
}
#[test]
fn test_output_name_sorting() {
let mut names = vec![
make_output_name("DP-2", None, None, None),
make_output_name("DP-1", None, None, None),
make_output_name("DP-3", Some("B"), Some("A"), Some("A")),
make_output_name("DP-3", Some("A"), Some("B"), Some("A")),
make_output_name("DP-3", Some("A"), Some("A"), Some("B")),
make_output_name("DP-3", None, Some("A"), Some("A")),
make_output_name("DP-3", Some("A"), None, Some("A")),
make_output_name("DP-3", Some("A"), Some("A"), None),
make_output_name("DP-5", Some("A"), Some("A"), Some("A")),
make_output_name("DP-4", Some("A"), Some("A"), Some("A")),
];
names.sort_by(|a, b| a.compare(b));
let names = names
.into_iter()
.map(|name| {
format!(
"{} | {}",
name.format_make_model_serial_or_connector(),
name.connector,
)
})
.collect::<Vec<_>>();
assert_debug_snapshot!(
names,
@r#"
[
"Unknown A A | DP-3",
"A Unknown A | DP-3",
"A A Unknown | DP-3",
"A A A | DP-4",
"A A A | DP-5",
"A A B | DP-3",
"A B A | DP-3",
"B A A | DP-3",
"DP-1 | DP-1",
"DP-2 | DP-2",
]
"#
);
}
}
-401
View File
@@ -1,401 +0,0 @@
use std::collections::HashSet;
use knuffel::errors::DecodeError;
use smithay::input::keyboard::Keysym;
use crate::utils::{expect_only_children, MergeWith};
use crate::{Action, Bind, Color, FloatOrInt, Key, Modifiers, Trigger};
#[derive(Debug, PartialEq)]
pub struct RecentWindows {
pub on: bool,
pub debounce_ms: u16,
pub open_delay_ms: u16,
pub highlight: MruHighlight,
pub previews: MruPreviews,
pub binds: Vec<Bind>,
}
impl Default for RecentWindows {
fn default() -> Self {
RecentWindows {
on: true,
debounce_ms: 750,
open_delay_ms: 150,
highlight: MruHighlight::default(),
previews: MruPreviews::default(),
binds: default_binds(),
}
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct RecentWindowsPart {
#[knuffel(child)]
pub on: bool,
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument))]
pub debounce_ms: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub open_delay_ms: Option<u16>,
#[knuffel(child)]
pub highlight: Option<MruHighlightPart>,
#[knuffel(child)]
pub previews: Option<MruPreviewsPart>,
#[knuffel(child)]
pub binds: Option<MruBinds>,
}
impl MergeWith<RecentWindowsPart> for RecentWindows {
fn merge_with(&mut self, part: &RecentWindowsPart) {
self.on |= part.on;
if part.off {
self.on = false;
}
merge_clone!((self, part), debounce_ms, open_delay_ms);
merge!((self, part), highlight, previews);
if let Some(part) = &part.binds {
// Remove existing binds matching any new bind.
self.binds
.retain(|bind| !part.0.iter().any(|new| new.key == bind.key));
// Add all new binds.
self.binds.extend(part.0.iter().cloned().map(Bind::from));
}
}
}
#[derive(Debug, PartialEq)]
pub struct MruHighlight {
pub active_color: Color,
pub urgent_color: Color,
pub padding: f64,
pub corner_radius: f64,
}
impl Default for MruHighlight {
fn default() -> Self {
Self {
active_color: Color::new_unpremul(0.6, 0.6, 0.6, 1.),
urgent_color: Color::new_unpremul(1., 0.6, 0.6, 1.),
padding: 30.,
corner_radius: 0.,
}
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct MruHighlightPart {
#[knuffel(child)]
pub active_color: Option<Color>,
#[knuffel(child)]
pub urgent_color: Option<Color>,
#[knuffel(child, unwrap(argument))]
pub padding: Option<FloatOrInt<0, 65535>>,
#[knuffel(child, unwrap(argument))]
pub corner_radius: Option<FloatOrInt<0, 65535>>,
}
impl MergeWith<MruHighlightPart> for MruHighlight {
fn merge_with(&mut self, part: &MruHighlightPart) {
merge_clone!((self, part), active_color, urgent_color);
merge!((self, part), padding, corner_radius);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MruPreviews {
pub max_height: f64,
pub max_scale: f64,
}
impl Default for MruPreviews {
fn default() -> Self {
Self {
max_height: 480.,
max_scale: 0.5,
}
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct MruPreviewsPart {
#[knuffel(child, unwrap(argument))]
pub max_height: Option<FloatOrInt<1, 65535>>,
#[knuffel(child, unwrap(argument))]
pub max_scale: Option<FloatOrInt<0, 1>>,
}
impl MergeWith<MruPreviewsPart> for MruPreviews {
fn merge_with(&mut self, part: &MruPreviewsPart) {
merge!((self, part), max_height, max_scale);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MruBind {
// MRU bind keys must have a modifier, this is enforced during parsing. The switcher will close
// once all modifiers are released.
pub key: Key,
pub action: MruAction,
pub allow_inhibiting: bool,
pub hotkey_overlay_title: Option<Option<String>>,
}
impl From<MruBind> for Bind {
fn from(x: MruBind) -> Self {
Self {
key: x.key,
action: Action::from(x.action),
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: x.allow_inhibiting,
hotkey_overlay_title: x.hotkey_overlay_title,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum MruDirection {
/// Most recently used to least.
#[default]
Forward,
/// Least recently used to most.
Backward,
}
#[derive(knuffel::DecodeScalar, Clone, Copy, Debug, Default, PartialEq)]
pub enum MruScope {
/// All windows.
#[default]
All,
/// Windows on the active output.
Output,
/// Windows on the active workspace.
Workspace,
}
#[derive(knuffel::DecodeScalar, Clone, Copy, Debug, Default, PartialEq)]
pub enum MruFilter {
/// All windows.
#[default]
#[knuffel(skip)]
All,
/// Windows with the same app id as the active window.
AppId,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub enum MruAction {
NextWindow(
#[knuffel(property(name = "scope"))] Option<MruScope>,
#[knuffel(property(name = "filter"), default)] MruFilter,
),
PreviousWindow(
#[knuffel(property(name = "scope"))] Option<MruScope>,
#[knuffel(property(name = "filter"), default)] MruFilter,
),
}
impl From<MruAction> for Action {
fn from(x: MruAction) -> Self {
match x {
MruAction::NextWindow(scope, filter) => Self::MruAdvance {
direction: MruDirection::Forward,
scope,
filter: Some(filter),
},
MruAction::PreviousWindow(scope, filter) => Self::MruAdvance {
direction: MruDirection::Backward,
scope,
filter: Some(filter),
},
}
}
}
#[derive(Debug, Default, PartialEq)]
pub struct MruBinds(pub Vec<MruBind>);
fn default_binds() -> Vec<Bind> {
let mut rv = Vec::new();
let mut push = |trigger, base_mod, filter| {
rv.push(Bind::from(MruBind {
key: Key {
trigger: Trigger::Keysym(trigger),
modifiers: base_mod,
},
action: MruAction::NextWindow(None, filter),
allow_inhibiting: true,
hotkey_overlay_title: None,
}));
rv.push(Bind::from(MruBind {
key: Key {
trigger: Trigger::Keysym(trigger),
modifiers: base_mod | Modifiers::SHIFT,
},
action: MruAction::PreviousWindow(None, filter),
allow_inhibiting: true,
hotkey_overlay_title: None,
}));
};
for base_mod in [Modifiers::ALT, Modifiers::COMPOSITOR] {
push(Keysym::Tab, base_mod, MruFilter::All);
push(Keysym::grave, base_mod, MruFilter::AppId);
}
rv
}
impl<S> knuffel::Decode<S> for MruBinds
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
expect_only_children(node, ctx);
let mut seen_keys = HashSet::new();
let mut binds = Vec::new();
for child in node.children() {
match MruBind::decode_node(child, ctx) {
Ok(bind) => {
if !seen_keys.insert(bind.key) {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"keybind",
"duplicate keybind",
));
continue;
}
binds.push(bind);
}
Err(e) => {
ctx.emit_error(e);
}
}
}
Ok(Self(binds))
}
}
impl<S> knuffel::Decode<S> for MruBind
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
for val in node.arguments.iter() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"no arguments expected for this node",
));
}
let key = node
.node_name
.parse::<Key>()
.map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?;
// A modifier is required because MRU remains on screen as long as any modifier is held.
if key.modifiers.is_empty() {
ctx.emit_error(DecodeError::unexpected(
&node.node_name,
"keybind",
"keybind must have a modifier key",
));
}
// FIXME: To support this, all the mods_with_mouse_binds()/mods_with_wheel_binds()/etc.
// will need to learn about recent-windows bindings.
if !matches!(key.trigger, Trigger::Keysym(_)) {
ctx.emit_error(DecodeError::unexpected(
&node.node_name,
"key",
"key must be a keyboard key (others are unsupported here for now)",
));
}
let mut allow_inhibiting = true;
let mut hotkey_overlay_title = None;
for (name, val) in &node.properties {
match &***name {
"allow-inhibiting" => {
allow_inhibiting = knuffel::traits::DecodeScalar::decode(val, ctx)?;
}
"hotkey-overlay-title" => {
hotkey_overlay_title = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
}
name_str => {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name_str.escape_default()),
));
}
}
}
let mut children = node.children();
// If the action is invalid but the key is fine, we still want to return something.
// That way, the parent can handle the existence of duplicate keybinds,
// even if their contents are not valid.
let dummy = Self {
key,
action: MruAction::NextWindow(None, MruFilter::All),
allow_inhibiting: true,
hotkey_overlay_title: None,
};
if let Some(child) = children.next() {
for unwanted_child in children {
ctx.emit_error(DecodeError::unexpected(
unwanted_child,
"node",
"only one action is allowed per keybind",
));
}
match MruAction::decode_node(child, ctx) {
Ok(action) => Ok(Self {
key,
action,
allow_inhibiting,
hotkey_overlay_title,
}),
Err(e) => {
ctx.emit_error(e);
Ok(dummy)
}
}
} else {
ctx.emit_error(DecodeError::missing(
node,
"expected an action for this keybind",
));
Ok(dummy)
}
}
}
-184
View File
@@ -1,28 +1,7 @@
use std::str::FromStr;
use knuffel::errors::DecodeError;
use miette::miette;
use regex::Regex;
mod merge_with;
pub use merge_with::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Percent(pub f64);
// MIN and MAX generics are only used during parsing to check the value.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct FloatOrInt<const MIN: i32, const MAX: i32>(pub f64);
/// Flag, with an optional explicit value.
///
/// Intended to be used as an `Option<MaybeBool>` field, as a tri-state:
/// - (missing): unset, `None`
/// - just `field`: set, `Some(true)`
/// - explicitly `field true` or `field false`: set, `Some(true)` or `Some(false)`
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Flag(#[knuffel(argument, default = true)] pub bool);
/// `Regex` that implements `PartialEq` by its string form.
#[derive(Debug, Clone)]
pub struct RegexEq(pub Regex);
@@ -42,166 +21,3 @@ impl FromStr for RegexEq {
Regex::from_str(s).map(Self)
}
}
impl FromStr for Percent {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((value, empty)) = s.split_once('%') else {
return Err(miette!("value must end with '%'"));
};
if !empty.is_empty() {
return Err(miette!("trailing characters after '%' are not allowed"));
}
let value: f64 = value.parse().map_err(|_| miette!("error parsing value"))?;
Ok(Percent(value / 100.))
}
}
impl<const MIN: i32, const MAX: i32> MergeWith<FloatOrInt<MIN, MAX>> for f64 {
fn merge_with(&mut self, part: &FloatOrInt<MIN, MAX>) {
*self = part.0;
}
}
impl MergeWith<Flag> for bool {
fn merge_with(&mut self, part: &Flag) {
*self = part.0;
}
}
impl<S: knuffel::traits::ErrorSpan, const MIN: i32, const MAX: i32> knuffel::DecodeScalar<S>
for FloatOrInt<MIN, MAX>
{
fn type_check(
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
ctx: &mut knuffel::decode::Context<S>,
) {
if let Some(type_name) = &type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
}
fn raw_decode(
val: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
match &**val {
knuffel::ast::Literal::Int(ref value) => match value.try_into() {
Ok(v) => {
if (MIN..=MAX).contains(&v) {
Ok(FloatOrInt(f64::from(v)))
} else {
ctx.emit_error(DecodeError::conversion(
val,
format!("value must be between {MIN} and {MAX}"),
));
Ok(FloatOrInt::default())
}
}
Err(e) => {
ctx.emit_error(DecodeError::conversion(val, e));
Ok(FloatOrInt::default())
}
},
knuffel::ast::Literal::Decimal(ref value) => match value.try_into() {
Ok(v) => {
if (f64::from(MIN)..=f64::from(MAX)).contains(&v) {
Ok(FloatOrInt(v))
} else {
ctx.emit_error(DecodeError::conversion(
val,
format!("value must be between {MIN} and {MAX}"),
));
Ok(FloatOrInt::default())
}
}
Err(e) => {
ctx.emit_error(DecodeError::conversion(val, e));
Ok(FloatOrInt::default())
}
},
_ => {
ctx.emit_error(DecodeError::unsupported(
val,
"Unsupported value, only numbers are recognized",
));
Ok(FloatOrInt::default())
}
}
}
}
pub fn expect_only_children<S>(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) where
S: knuffel::traits::ErrorSpan,
{
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
for val in node.arguments.iter() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"no arguments expected for this node",
))
}
for name in node.properties.keys() {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
"no properties expected for this node",
))
}
}
pub fn parse_arg_node<S: knuffel::traits::ErrorSpan, T: knuffel::traits::DecodeScalar<S>>(
name: &str,
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<T, DecodeError<S>> {
let mut iter_args = node.arguments.iter();
let val = iter_args.next().ok_or_else(|| {
DecodeError::missing(node, format!("additional argument `{name}` is required"))
})?;
let value = knuffel::traits::DecodeScalar::decode(val, ctx)?;
if let Some(val) = iter_args.next() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"unexpected argument",
));
}
for name in node.properties.keys() {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name.escape_default()),
));
}
for child in node.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
Ok(value)
}
-18
View File
@@ -1,18 +0,0 @@
pub trait MergeWith<T> {
fn merge_with(&mut self, part: &T);
fn merged_with(mut self, part: &T) -> Self
where
Self: Sized,
{
self.merge_with(part);
self
}
fn from_part(part: &T) -> Self
where
Self: Default + Sized,
{
Self::default().merged_with(part)
}
}
-163
View File
@@ -1,163 +0,0 @@
use niri_ipc::ColumnDisplay;
use crate::appearance::{
BackgroundEffect, BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule,
TabIndicatorRule,
};
use crate::layout::DefaultPresetSize;
use crate::utils::{MergeWith, RegexEq};
use crate::FloatOrInt;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct WindowRule {
#[knuffel(children(name = "match"))]
pub matches: Vec<Match>,
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,
// Rules applied at initial configure.
#[knuffel(child)]
pub default_column_width: Option<DefaultPresetSize>,
#[knuffel(child)]
pub default_window_height: Option<DefaultPresetSize>,
#[knuffel(child, unwrap(argument))]
pub open_on_output: Option<String>,
#[knuffel(child, unwrap(argument))]
pub open_on_workspace: Option<String>,
#[knuffel(child, unwrap(argument))]
pub open_maximized: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_maximized_to_edges: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_fullscreen: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_floating: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_focused: Option<bool>,
// Rules applied dynamically.
#[knuffel(child, unwrap(argument))]
pub min_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub min_height: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_height: Option<u16>,
#[knuffel(child, default)]
pub focus_ring: BorderRule,
#[knuffel(child, default)]
pub border: BorderRule,
#[knuffel(child, default)]
pub shadow: ShadowRule,
#[knuffel(child, default)]
pub tab_indicator: TabIndicatorRule,
#[knuffel(child, unwrap(argument))]
pub draw_border_with_background: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, unwrap(argument))]
pub clip_to_geometry: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
#[knuffel(child, unwrap(argument))]
pub variable_refresh_rate: Option<bool>,
#[knuffel(child, unwrap(argument, str))]
pub default_column_display: Option<ColumnDisplay>,
#[knuffel(child)]
pub default_floating_position: Option<FloatingPosition>,
#[knuffel(child, unwrap(argument))]
pub scroll_factor: Option<FloatOrInt<0, 100>>,
#[knuffel(child, unwrap(argument))]
pub tiled_state: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
#[knuffel(child, default)]
pub popups: PopupsRule,
}
/// Rules for popup surfaces.
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct PopupsRule {
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
}
/// Resolved popup-specific rules.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct ResolvedPopupsRules {
/// Extra opacity to draw popups with.
pub opacity: Option<f32>,
/// Corner radius to assume the popups have.
pub geometry_corner_radius: Option<CornerRadius>,
/// Background effect configuration for popups.
pub background_effect: BackgroundEffect,
}
impl MergeWith<PopupsRule> for ResolvedPopupsRules {
fn merge_with(&mut self, part: &PopupsRule) {
if let Some(x) = part.opacity {
self.opacity = Some(x);
}
if let Some(x) = part.geometry_corner_radius {
self.geometry_corner_radius = Some(x);
}
self.background_effect.merge_with(&part.background_effect);
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct Match {
#[knuffel(property, str)]
pub app_id: Option<RegexEq>,
#[knuffel(property, str)]
pub title: Option<RegexEq>,
#[knuffel(property)]
pub is_active: Option<bool>,
#[knuffel(property)]
pub is_focused: Option<bool>,
#[knuffel(property)]
pub is_active_in_column: Option<bool>,
#[knuffel(property)]
pub is_floating: Option<bool>,
#[knuffel(property)]
pub is_window_cast_target: Option<bool>,
#[knuffel(property)]
pub is_urgent: Option<bool>,
#[knuffel(property)]
pub at_startup: Option<bool>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct FloatingPosition {
#[knuffel(property)]
pub x: FloatOrInt<-65535, 65535>,
#[knuffel(property)]
pub y: FloatOrInt<-65535, 65535>,
#[knuffel(property, default)]
pub relative_to: RelativeTo,
}
#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum RelativeTo {
#[default]
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Top,
Bottom,
Left,
Right,
}
-97
View File
@@ -1,97 +0,0 @@
use knuffel::errors::DecodeError;
use crate::LayoutPart;
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Workspace {
#[knuffel(argument)]
pub name: WorkspaceName,
#[knuffel(child, unwrap(argument))]
pub open_on_output: Option<String>,
#[knuffel(child)]
pub layout: Option<WorkspaceLayoutPart>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceName(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct WorkspaceLayoutPart(pub LayoutPart);
impl<S: knuffel::traits::ErrorSpan> knuffel::Decode<S> for WorkspaceLayoutPart {
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
for child in node.children() {
let name = &**child.node_name;
// Check for disallowed properties.
//
// - empty-workspace-above-first is a monitor-level concept.
// - insert-hint customization could make sense for workspaces, however currently it is
// also handled at the monitor level (since insert hints in-between workspaces are a
// monitor-level concept), so for now this config option would do nothing.
if matches!(name, "empty-workspace-above-first" | "insert-hint") {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("node `{name}` is not allowed inside `workspace.layout`"),
));
}
}
LayoutPart::decode_node(node, ctx).map(Self)
}
}
impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceName {
fn type_check(
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
ctx: &mut knuffel::decode::Context<S>,
) {
if let Some(type_name) = &type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
}
fn raw_decode(
val: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<WorkspaceName, DecodeError<S>> {
#[derive(Debug)]
struct WorkspaceNameSet(Vec<String>);
match &**val {
knuffel::ast::Literal::String(ref s) => {
let mut name_set: Vec<String> = match ctx.get::<WorkspaceNameSet>() {
Some(h) => h.0.clone(),
None => Vec::new(),
};
if name_set.iter().any(|name| name.eq_ignore_ascii_case(s)) {
ctx.emit_error(DecodeError::unexpected(
val,
"named workspace",
format!("duplicate named workspace: {s}"),
));
return Ok(Self(String::new()));
}
name_set.push(s.to_string());
ctx.set(WorkspaceNameSet(name_set));
Ok(Self(s.clone().into()))
}
_ => {
ctx.emit_error(DecodeError::unsupported(
val,
"workspace names must be strings",
));
Ok(Self(String::new()))
}
}
}
}
+5 -4
View File
@@ -1,5 +1,5 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
struct KdlCodeBlock {
filename: String,
@@ -56,7 +56,7 @@ fn extract_kdl_from_file(file_contents: &str, filename: &str) -> Vec<KdlCodeBloc
#[test]
fn wiki_docs_parses() {
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../docs/wiki");
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki");
let code_blocks = fs::read_dir(wiki_dir)
.unwrap()
@@ -84,7 +84,7 @@ fn wiki_docs_parses() {
must_fail,
} in code_blocks
{
if let Err(error) = niri_config::Config::parse(Path::new(&filename), &code).config {
if let Err(error) = niri_config::Config::parse(&filename, &code) {
if !must_fail {
errors.push(format!(
"Error parsing wiki KDL code block at {}:{}: {:?}",
@@ -95,7 +95,8 @@ fn wiki_docs_parses() {
}
} else if must_fail {
errors.push(format!(
"Expected error parsing wiki KDL code block at {filename}:{line_number}",
"Expected error parsing wiki KDL code block at {}:{}",
filename, line_number
));
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
[dependencies]
clap = { workspace = true, optional = true }
schemars = { version = "1.2.1", optional = true }
schemars = { version = "0.8.22", optional = true }
serde.workspace = true
serde_json.workspace = true
+2 -2
View File
@@ -1,6 +1,6 @@
# niri-ipc
Types and helpers for interfacing with the [niri](https://github.com/niri-wm/niri) Wayland compositor.
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
## Backwards compatibility
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=26.4.0"
niri-ipc = "=25.2.0"
```
+23 -706
View File
@@ -1,23 +1,8 @@
//! Types for communicating with niri via IPC.
//!
//! After connecting to the niri socket, you can send [`Request`]s. Niri will process them one by
//! one, in order, and to each request it will respond with a single [`Reply`], which is a `Result`
//! wrapping a [`Response`].
//!
//! If you send a [`Request::EventStream`], niri will *stop* reading subsequent [`Request`]s, and
//! will start continuously writing compositor [`Event`]s to the socket. If you'd like to read an
//! event stream and write more requests at the same time, you need to use two IPC sockets.
//!
//! <div class="warning">
//!
//! Requests are *always* processed separately. Time passes between requests, even when sending
//! multiple requests to the socket at once. For example, sending [`Request::Workspaces`] and
//! [`Request::Windows`] together may not return consistent results (e.g. a window may open on a
//! new workspace in-between the two responses). This goes for actions too: sending
//! [`Action::FocusWindow`] and <code>[Action::CloseWindow] { id: None }</code> together may close
//! the wrong window because a different window got focused in-between these requests.
//!
//! </div>
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
//! can keep reading [`Event`]s from the socket after the response.
//!
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
@@ -27,9 +12,7 @@
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
//! 4. You can keep writing [`Request`]s, each on a single line, and read [`Reply`]s, also each on a
//! separate line.
//! 5. After you request an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
//! on a single line each.
//!
//! ## Backwards compatibility
@@ -41,7 +24,7 @@
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=26.4.0"
//! niri-ipc = "=25.2.0"
//! ```
//!
//! ## Features
@@ -54,7 +37,6 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::time::Duration;
use serde::{Deserialize, Serialize};
@@ -115,10 +97,6 @@ pub enum Request {
EventStream,
/// Respond with an error (for testing error handling).
ReturnError,
/// Request information about the overview.
OverviewState,
/// Request information about screencasts.
Casts,
}
/// Reply from niri to client.
@@ -161,18 +139,6 @@ pub enum Response {
PickedColor(Option<PickedColor>),
/// Output configuration change result.
OutputConfigChanged(OutputConfigChanged),
/// Information about the overview.
OverviewState(Overview),
/// Information about screencasts.
Casts(Vec<Cast>),
}
/// Overview information.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Overview {
/// Whether the overview is currently open.
pub is_open: bool,
}
/// Color picked from the screen.
@@ -208,12 +174,6 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
command: Vec<String>,
},
/// Spawn a command through the shell.
SpawnSh {
/// Command to run.
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
command: String,
},
/// Do a screen transition.
DoScreenTransition {
/// Delay in milliseconds for the screen to freeze before starting the transition.
@@ -225,14 +185,6 @@ pub enum Action {
/// Whether to show the mouse pointer by default in the screenshot UI.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
/// Path to save the screenshot to.
///
/// The path must be absolute, otherwise an error is returned.
///
/// If `None`, the screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))]
path: Option<String>,
},
/// Screenshot the focused screen.
ScreenshotScreen {
@@ -245,14 +197,6 @@ pub enum Action {
/// Whether to include the mouse pointer in the screenshot.
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
show_pointer: bool,
/// Path to save the screenshot to.
///
/// The path must be absolute, otherwise an error is returned.
///
/// If `None`, the screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))]
path: Option<String>,
},
/// Screenshot a window.
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
@@ -267,21 +211,6 @@ pub enum Action {
/// The screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
write_to_disk: bool,
/// Whether to include the mouse pointer in the screenshot.
///
/// The pointer will be included only if the window is currently receiving pointer input
/// (usually this means the pointer is on top of the window).
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = false))]
show_pointer: bool,
/// Path to save the screenshot to.
///
/// The path must be absolute, otherwise an error is returned.
///
/// If `None`, the screenshot is saved according to the `screenshot-path` config setting.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))]
path: Option<String>,
},
/// Enable or disable the keyboard shortcuts inhibitor (if any) for the focused surface.
ToggleKeyboardShortcutsInhibit {},
@@ -374,7 +303,7 @@ pub enum Action {
FocusWindowUpOrColumnLeft {},
/// Focus the window above or the column to the right.
FocusWindowUpOrColumnRight {},
/// Focus the window or the workspace below.
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceDown {},
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp {},
@@ -440,7 +369,7 @@ pub enum Action {
},
/// Consume the window to the right into the focused column.
ConsumeWindowIntoColumn {},
/// Expel the bottom window from the focused column.
/// Expel the focused window from the column.
ExpelWindowFromColumn {},
/// Swap focused window with one to the right.
SwapWindowRight {},
@@ -468,8 +397,6 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Center all fully visible columns on the screen.
CenterVisibleColumns {},
/// Focus the workspace below.
FocusWorkspaceDown {},
/// Focus the workspace above.
@@ -483,23 +410,9 @@ pub enum Action {
/// Focus the previous workspace.
FocusWorkspacePrevious {},
/// Move the focused window to the workspace below.
MoveWindowToWorkspaceDown {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the window to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
MoveWindowToWorkspaceDown {},
/// Move the focused window to the workspace above.
MoveWindowToWorkspaceUp {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the window to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
MoveWindowToWorkspaceUp {},
/// Move a window to a workspace.
#[cfg_attr(
feature = "clap",
@@ -525,35 +438,14 @@ pub enum Action {
focus: bool,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
MoveColumnToWorkspaceDown {},
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp {
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
MoveColumnToWorkspaceUp {},
/// Move the focused column to a workspace by reference (index or name).
MoveColumnToWorkspace {
/// Reference (index or name) of the workspace to move the column to.
#[cfg_attr(feature = "clap", arg())]
reference: WorkspaceReferenceArg,
/// Whether the focus should follow the target workspace.
///
/// If `true` (the default), the focus will follow the column to the new workspace. If
/// `false`, the focus will remain on the original workspace.
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
focus: bool,
},
/// Move the focused workspace down.
MoveWorkspaceDown {},
@@ -713,8 +605,6 @@ pub enum Action {
},
/// Switch between preset column widths.
SwitchPresetColumnWidth {},
/// Switch between preset column widths backwards.
SwitchPresetColumnWidthBack {},
/// Switch between preset window widths.
SwitchPresetWindowWidth {
/// Id of the window whose width to switch.
@@ -723,14 +613,6 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset window widths backwards.
SwitchPresetWindowWidthBack {
/// Id of the window whose width to switch.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset window heights.
SwitchPresetWindowHeight {
/// Id of the window whose height to switch.
@@ -739,24 +621,8 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Switch between preset window heights backwards.
SwitchPresetWindowHeightBack {
/// Id of the window whose height to switch.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Toggle the maximized state of the focused column.
MaximizeColumn {},
/// Toggle the maximized-to-edges state of the focused window.
MaximizeWindowToEdges {
/// Id of the window to maximize.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
@@ -849,14 +715,14 @@ pub enum Action {
/// How to change the X position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_hyphen_values = true)
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
x: PositionChange,
/// How to change the Y position.
#[cfg_attr(
feature = "clap",
arg(short, long, default_value = "+0", allow_hyphen_values = true)
arg(short, long, default_value = "+0", allow_negative_numbers = true)
)]
y: PositionChange,
},
@@ -898,51 +764,8 @@ pub enum Action {
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
/// Stop a PipeWire screencast.
///
/// wlr-screencopy screencasts cannot currently be stopped via IPC.
StopCast {
/// Session ID of the screencast to stop.
///
/// If the session has multiple screencast streams, this will stop all of them.
#[cfg_attr(feature = "clap", arg(long))]
session_id: u64,
},
/// Toggle (open/close) the Overview.
/// Toggle the Overview.
ToggleOverview {},
/// Open the Overview.
OpenOverview {},
/// Close the Overview.
CloseOverview {},
/// Toggle urgent status of a window.
ToggleWindowUrgent {
/// Id of the window to toggle urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Set urgent status of a window.
SetWindowUrgent {
/// Id of the window to set urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Unset urgent status of a window.
UnsetWindowUrgent {
/// Id of the window to unset urgent.
#[cfg_attr(feature = "clap", arg(long))]
id: u64,
},
/// Reload the config file.
///
/// Can be useful for scripts changing the config file, to avoid waiting the small duration for
/// niri's config file watcher to notice the changes.
LoadConfigFile {
/// Path of a new config file to load.
///
/// If unset, reloads the current config file.
#[cfg_attr(feature = "clap", arg(long))]
path: Option<String>,
},
}
/// Change in window or column size.
@@ -965,12 +788,8 @@ pub enum SizeChange {
pub enum PositionChange {
/// Set the position in logical pixels.
SetFixed(f64),
/// Set the position as a proportion of the working area.
SetProportion(f64),
/// Add or subtract to the current position in logical pixels.
AdjustFixed(f64),
/// Add or subtract to the current position as a proportion of the working area.
AdjustProportion(f64),
}
/// Workspace reference (id, index or name) to operate on.
@@ -1028,51 +847,6 @@ pub enum OutputAction {
#[cfg_attr(feature = "clap", arg())]
mode: ModeToSet,
},
/// Set a custom output mode.
CustomMode {
/// Custom mode to set.
#[cfg_attr(feature = "clap", arg())]
mode: ConfiguredMode,
},
/// Set a custom VESA CVT modeline.
#[cfg_attr(feature = "clap", arg())]
Modeline {
/// The rate at which pixels are drawn in MHz.
#[cfg_attr(feature = "clap", arg())]
clock: f64,
/// Horizontal active pixels.
#[cfg_attr(feature = "clap", arg())]
hdisplay: u16,
/// Horizontal sync pulse start position in pixels.
#[cfg_attr(feature = "clap", arg())]
hsync_start: u16,
/// Horizontal sync pulse end position in pixels.
#[cfg_attr(feature = "clap", arg())]
hsync_end: u16,
/// Total horizontal number of pixels before resetting the horizontal drawing position to
/// zero.
#[cfg_attr(feature = "clap", arg())]
htotal: u16,
/// Vertical active pixels.
#[cfg_attr(feature = "clap", arg())]
vdisplay: u16,
/// Vertical sync pulse start position in pixels.
#[cfg_attr(feature = "clap", arg())]
vsync_start: u16,
/// Vertical sync pulse end position in pixels.
#[cfg_attr(feature = "clap", arg())]
vsync_end: u16,
/// Total vertical number of pixels before resetting the vertical drawing position to zero.
#[cfg_attr(feature = "clap", arg())]
vtotal: u16,
/// Horizontal sync polarity: "+hsync" or "-hsync".
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
hsync_polarity: HSyncPolarity,
/// Vertical sync polarity: "+vsync" or "-vsync".
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
vsync_polarity: VSyncPolarity,
},
/// Set the output scale.
Scale {
/// Scale factor to set, or "auto" for automatic selection.
@@ -1121,26 +895,6 @@ pub struct ConfiguredMode {
pub refresh: Option<f64>,
}
/// Modeline horizontal syncing polarity.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum HSyncPolarity {
/// Positive polarity.
PHSync,
/// Negative polarity.
NHSync,
}
/// Modeline vertical syncing polarity.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum VSyncPolarity {
/// Positive polarity.
PVSync,
/// Negative polarity.
NVSync,
}
/// Output scale to set.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -1218,8 +972,6 @@ pub struct Output {
///
/// `None` if the output is disabled.
pub current_mode: Option<usize>,
/// Whether the current_mode is a custom mode.
pub is_custom_mode: bool,
/// Whether the output supports variable refresh rate.
pub vrr_supported: bool,
/// Whether variable refresh rate is enabled on the output.
@@ -1322,74 +1074,6 @@ pub struct Window {
///
/// If the window isn't floating then it is in the tiling layout.
pub is_floating: bool,
/// Whether this window requests your attention.
pub is_urgent: bool,
/// Position- and size-related properties of the window.
pub layout: WindowLayout,
/// Timestamp when the window was most recently focused.
///
/// This timestamp is intended for most-recently-used window switchers, i.e. Alt-Tab. It only
/// updates after some debounce time so that quick window switching doesn't mark intermediate
/// windows as recently focused.
///
/// The timestamp comes from the monotonic clock.
pub focus_timestamp: Option<Timestamp>,
}
/// A moment in time.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Timestamp {
/// Number of whole seconds.
pub secs: u64,
/// Fractional part of the timestamp in nanoseconds (10<sup>-9</sup> seconds).
pub nanos: u32,
}
/// Position- and size-related properties of a [`Window`].
///
/// Optional properties will be unset for some windows, do not rely on them being present. Whether
/// some optional properties are present or absent for certain window types may change across niri
/// releases.
///
/// All sizes and positions are in *logical pixels* unless stated otherwise. Logical sizes may be
/// fractional. For example, at 1.25 monitor scale, a 2-physical-pixel-wide window border is 1.6
/// logical pixels wide.
///
/// This struct contains positions and sizes both for full tiles ([`Self::tile_size`],
/// [`Self::tile_pos_in_workspace_view`]) and the window geometry ([`Self::window_size`],
/// [`Self::window_offset_in_tile`]). For visual displays, use the tile properties, as they
/// correspond to what the user visually considers "window". The window properties on the other
/// hand are mainly useful when you need to know the underlying Wayland window sizes, e.g. for
/// application debugging.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct WindowLayout {
/// Location of a tiled window within a workspace: (column index, tile index in column).
///
/// The indices are 1-based, i.e. the leftmost column is at index 1 and the topmost tile in a
/// column is at index 1. This is consistent with [`Action::FocusColumn`] and
/// [`Action::FocusWindowInColumn`].
pub pos_in_scrolling_layout: Option<(usize, usize)>,
/// Size of the tile this window is in, including decorations like borders.
pub tile_size: (f64, f64),
/// Size of the window's visual geometry itself.
///
/// Does not include niri decorations like borders.
///
/// Currently, Wayland toplevel windows can only be integer-sized in logical pixels, even
/// though it doesn't necessarily align to physical pixels.
pub window_size: (i32, i32),
/// Tile position within the current view of the workspace.
///
/// This is the same "workspace view" as in gradients' `relative-to` in the niri config.
pub tile_pos_in_workspace_view: Option<(f64, f64)>,
/// Location of the window's visual geometry within its tile.
///
/// This includes things like border sizes. For fullscreened fixed-size windows this includes
/// the distance from the corner of the black backdrop to the corner of the (centered) window
/// contents.
pub window_offset_in_tile: (f64, f64),
}
/// Output configuration change result.
@@ -1429,8 +1113,6 @@ pub struct Workspace {
///
/// Can be `None` if no outputs are currently connected.
pub output: Option<String>,
/// Whether the workspace currently has an urgent window in its output.
pub is_urgent: bool,
/// Whether the workspace is currently active on its output.
///
/// Every output has one active workspace, the one that is currently visible on that output.
@@ -1493,78 +1175,6 @@ pub struct LayerSurface {
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
}
/// A screencast.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct Cast {
/// Stream ID of the screencast that uniquely identifies it.
pub stream_id: u64,
/// Session ID of the screencast.
///
/// A session can have multiple screencast streams. Then multiple `Cast`s will have the same
/// `session_id`. Though, usually there's only one stream per session.
///
/// Do not confuse `session_id` with [`stream_id`](Self::stream_id).
pub session_id: u64,
/// Kind of this screencast.
pub kind: CastKind,
/// Target being captured.
pub target: CastTarget,
/// Whether this is a Dynamic Cast Target screencast.
///
/// Meaning that actions like `SetDynamicCastWindow` will act on this screencast.
///
/// Keep in mind that the target can change even if this is `false`.
pub is_dynamic_target: bool,
/// Whether the cast is currently streaming frames.
///
/// This can be `false` for example when switching away to a different scene in OBS, which
/// pauses the stream.
pub is_active: bool,
/// Process ID of the screencast consumer, if known.
///
/// Currently, only wlr-screencopy screencasts can have a pid.
pub pid: Option<i32>,
/// PipeWire node ID of the screencast stream.
///
/// This is `None` for wlr-screencopy casts, and also for PipeWire casts before the node is
/// created (when the cast is just starting up).
pub pw_node_id: Option<u32>,
}
/// Kind of screencast.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum CastKind {
/// PipeWire screencast, typically via xdg-desktop-portal-gnome.
PipeWire,
/// wlr-screencopy protocol screencast.
///
/// Tools like wf-recorder, and the xdg-desktop-portal-wlr portal.
///
/// Only wlr-screencopy with damage tracking is reported here. Screencopy without damage is
/// treated as a regular screenshot and not reported as a screencast.
WlrScreencopy,
}
/// Target of a screencast.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum CastTarget {
/// The target is not yet set, or was cleared.
Nothing {},
/// Casting an output.
Output {
/// Name of the screencasted output.
name: String,
},
/// Casting a window.
Window {
/// ID of the screencasted window.
id: u64,
},
}
/// A compositor event.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -1577,13 +1187,6 @@ pub enum Event {
/// workspaces are missing from here, then they were deleted.
workspaces: Vec<Workspace>,
},
/// The workspace urgency changed.
WorkspaceUrgencyChanged {
/// Id of the workspace.
id: u64,
/// Whether this workspace has an urgent window.
urgent: bool,
},
/// A workspace was activated on an output.
///
/// This doesn't always mean the workspace became focused, just that it's now the active
@@ -1631,29 +1234,6 @@ pub enum Event {
/// Id of the newly focused window, or `None` if no window is now focused.
id: Option<u64>,
},
/// Window focus timestamp changed.
///
/// This event is separate from [`Event::WindowFocusChanged`] because the focus timestamp only
/// updates after some debounce time so that quick window switching doesn't mark intermediate
/// windows as recently focused.
WindowFocusTimestampChanged {
/// Id of the window.
id: u64,
/// The new focus timestamp.
focus_timestamp: Option<Timestamp>,
},
/// Window urgency changed.
WindowUrgencyChanged {
/// Id of the window.
id: u64,
/// The new urgency state of the window.
urgent: bool,
},
/// The layout of one or more windows has changed.
WindowLayoutsChanged {
/// Pairs consisting of a window id and new layout information for the window.
changes: Vec<(u64, WindowLayout)>,
},
/// The configured keyboard layouts have changed.
KeyboardLayoutsChanged {
/// The new keyboard layout configuration.
@@ -1664,62 +1244,6 @@ pub enum Event {
/// Index of the newly active layout.
idx: u8,
},
/// The overview was opened or closed.
OverviewOpenedOrClosed {
/// The new state of the overview.
is_open: bool,
},
/// The configuration was reloaded.
///
/// You will always receive this event when connecting to the event stream, indicating the last
/// config load attempt.
ConfigLoaded {
/// Whether the loading failed.
///
/// For example, the config file couldn't be parsed.
failed: bool,
},
/// A screenshot was captured.
ScreenshotCaptured {
/// The file path where the screenshot was saved, if it was written to disk.
///
/// If `None`, the screenshot was either only copied to the clipboard, or the path couldn't
/// be converted to a `String` (e.g. contained invalid UTF-8 bytes).
path: Option<String>,
},
/// The screencasts have changed.
CastsChanged {
/// The new screencast information.
///
/// This configuration completely replaces the previous configuration. I.e. if any casts
/// are missing from here, then they were stopped.
casts: Vec<Cast>,
},
/// A screencast started, or an existing cast changed.
CastStartedOrChanged {
/// The cast that started or changed.
cast: Cast,
},
/// A screencast stopped.
CastStopped {
/// Stream ID of the stopped screencast.
stream_id: u64,
},
}
impl From<Duration> for Timestamp {
fn from(value: Duration) -> Self {
Timestamp {
secs: value.as_secs(),
nanos: value.subsec_nanos(),
}
}
}
impl From<Timestamp> for Duration {
fn from(value: Timestamp) -> Self {
Duration::new(value.secs, value.nanos)
}
}
impl FromStr for WorkspaceReferenceArg {
@@ -1784,38 +1308,17 @@ impl FromStr for PositionChange {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err("trailing characters after '%' are not allowed");
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err("value is missing"),
}
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustFixed(value))
}
None => {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err("value is missing"),
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err("value is missing"),
}
}
}
@@ -1868,20 +1371,6 @@ impl FromStr for Transform {
}
}
impl FromStr for Layer {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"background" => Ok(Self::Background),
"bottom" => Ok(Self::Bottom),
"top" => Ok(Self::Top),
"overlay" => Ok(Self::Overlay),
_ => Err("invalid layer, can be \"background\", \"bottom\", \"top\" or \"overlay\""),
}
}
}
impl FromStr for ModeToSet {
type Err = &'static str;
@@ -1923,30 +1412,6 @@ impl FromStr for ConfiguredMode {
}
}
impl FromStr for HSyncPolarity {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"+hsync" => Ok(Self::PHSync),
"-hsync" => Ok(Self::NHSync),
_ => Err(r#"invalid horizontal sync polarity, can be "+hsync" or "-hsync"#),
}
}
}
impl FromStr for VSyncPolarity {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"+vsync" => Ok(Self::PVSync),
"-vsync" => Ok(Self::NVSync),
_ => Err(r#"invalid vertical sync polarity, can be "+vsync" or "-vsync"#),
}
}
}
impl FromStr for ScaleToSet {
type Err = &'static str;
@@ -1959,151 +1424,3 @@ impl FromStr for ScaleToSet {
Ok(Self::Specific(scale))
}
}
macro_rules! ensure {
($cond:expr, $fmt:literal $($arg:tt)* ) => {
if !$cond {
return Err(format!($fmt $($arg)*));
}
};
}
impl OutputAction {
/// Validates some required constraints on the modeline and custom mode.
pub fn validate(&self) -> Result<(), String> {
match self {
OutputAction::Modeline {
hdisplay,
hsync_start,
hsync_end,
htotal,
vdisplay,
vsync_start,
vsync_end,
vtotal,
..
} => {
ensure!(
hdisplay < hsync_start,
"hdisplay {} must be < hsync_start {}",
hdisplay,
hsync_start
);
ensure!(
hsync_start < hsync_end,
"hsync_start {} must be < hsync_end {}",
hsync_start,
hsync_end
);
ensure!(
hsync_end < htotal,
"hsync_end {} must be < htotal {}",
hsync_end,
htotal
);
ensure!(0 < *htotal, "htotal {} must be > 0", htotal);
ensure!(
vdisplay < vsync_start,
"vdisplay {} must be < vsync_start {}",
vdisplay,
vsync_start
);
ensure!(
vsync_start < vsync_end,
"vsync_start {} must be < vsync_end {}",
vsync_start,
vsync_end
);
ensure!(
vsync_end < vtotal,
"vsync_end {} must be < vtotal {}",
vsync_end,
vtotal
);
ensure!(0 < *vtotal, "vtotal {} must be > 0", vtotal);
Ok(())
}
OutputAction::CustomMode {
mode: ConfiguredMode { refresh, .. },
} => {
if refresh.is_none() {
return Err("refresh rate is required for custom modes".to_string());
}
if let Some(refresh) = refresh {
if *refresh <= 0. {
return Err(format!("custom mode refresh rate {refresh} must be > 0"));
}
}
Ok(())
}
_ => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_size_change() {
assert_eq!(
"10".parse::<SizeChange>().unwrap(),
SizeChange::SetFixed(10),
);
assert_eq!(
"+10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(10),
);
assert_eq!(
"-10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(-10),
);
assert_eq!(
"10%".parse::<SizeChange>().unwrap(),
SizeChange::SetProportion(10.),
);
assert_eq!(
"+10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(10.),
);
assert_eq!(
"-10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(-10.),
);
assert!("-".parse::<SizeChange>().is_err());
assert!("10% ".parse::<SizeChange>().is_err());
}
#[test]
fn parse_position_change() {
assert_eq!(
"10".parse::<PositionChange>().unwrap(),
PositionChange::SetFixed(10.),
);
assert_eq!(
"+10".parse::<PositionChange>().unwrap(),
PositionChange::AdjustFixed(10.),
);
assert_eq!(
"-10".parse::<PositionChange>().unwrap(),
PositionChange::AdjustFixed(-10.),
);
assert_eq!(
"10%".parse::<PositionChange>().unwrap(),
PositionChange::SetProportion(10.)
);
assert_eq!(
"+10%".parse::<PositionChange>().unwrap(),
PositionChange::AdjustProportion(10.)
);
assert_eq!(
"-10%".parse::<PositionChange>().unwrap(),
PositionChange::AdjustProportion(-10.)
);
assert!("-".parse::<PositionChange>().is_err());
assert!("10% ".parse::<PositionChange>().is_err());
}
}
+18 -42
View File
@@ -16,7 +16,7 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
/// and serialization/deserialization of messages.
pub struct Socket {
stream: BufReader<UnixStream>,
stream: UnixStream,
}
impl Socket {
@@ -37,7 +37,6 @@ impl Socket {
/// Connects to the niri IPC socket at the given path.
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
let stream = UnixStream::connect(path.as_ref())?;
let stream = BufReader::new(stream);
Ok(Self { stream })
}
@@ -48,54 +47,31 @@ impl Socket {
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
/// * `Ok(Err(message))`: error message from niri
/// * `Err(error)`: error communicating with niri
pub fn send(&mut self, request: Request) -> io::Result<Reply> {
///
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
/// otherwise.
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
let Self { mut stream } = self;
let mut buf = serde_json::to_string(&request).unwrap();
buf.push('\n');
self.stream.get_mut().write_all(buf.as_bytes())?;
stream.write_all(buf.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
let mut reader = BufReader::new(stream);
buf.clear();
self.stream.read_line(&mut buf)?;
reader.read_line(&mut buf)?;
let reply = serde_json::from_str(&buf)?;
Ok(reply)
}
/// Starts reading event stream [`Event`]s from the socket.
///
/// The returned function will block until the next [`Event`] arrives, then return it.
///
/// Use this only after requesting an [`EventStream`][Request::EventStream].
///
/// # Examples
///
/// ```no_run
/// use niri_ipc::{Request, Response};
/// use niri_ipc::socket::Socket;
///
/// fn main() -> std::io::Result<()> {
/// let mut socket = Socket::connect()?;
///
/// let reply = socket.send(Request::EventStream)?;
/// if matches!(reply, Ok(Response::Handled)) {
/// let mut read_event = socket.read_events();
/// while let Ok(event) = read_event() {
/// println!("Received event: {event:?}");
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub fn read_events(self) -> impl FnMut() -> io::Result<Event> {
let Self { mut stream } = self;
let _ = stream.get_mut().shutdown(Shutdown::Write);
let mut buf = String::new();
move || {
let events = move || {
buf.clear();
stream.read_line(&mut buf)?;
reader.read_line(&mut buf)?;
let event = serde_json::from_str(&buf)?;
Ok(event)
}
};
Ok((reply, events))
}
}
+1 -130
View File
@@ -9,7 +9,7 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use crate::{Cast, Event, KeyboardLayouts, Window, Workspace};
use crate::{Event, KeyboardLayouts, Window, Workspace};
/// Part of the state communicated via the event stream.
pub trait EventStreamStatePart {
@@ -40,15 +40,6 @@ pub struct EventStreamState {
/// State of the keyboard layouts.
pub keyboard_layouts: KeyboardLayoutsState,
/// State of the overview.
pub overview: OverviewState,
/// State of the config.
pub config: ConfigState,
/// State of screencasts.
pub casts: CastsState,
}
/// The workspaces state communicated over the event stream.
@@ -72,36 +63,12 @@ pub struct KeyboardLayoutsState {
pub keyboard_layouts: Option<KeyboardLayouts>,
}
/// The overview state communicated over the event stream.
#[derive(Debug, Default)]
pub struct OverviewState {
/// Whether the overview is currently open.
pub is_open: bool,
}
/// The config state communicated over the event stream.
#[derive(Debug, Default)]
pub struct ConfigState {
/// Whether the last config load attempt had failed.
pub failed: bool,
}
/// The casts state communicated over the event stream.
#[derive(Debug, Default)]
pub struct CastsState {
/// Map from a stream id to the screencast.
pub casts: HashMap<u64, Cast>,
}
impl EventStreamStatePart for EventStreamState {
fn replicate(&self) -> Vec<Event> {
let mut events = Vec::new();
events.extend(self.workspaces.replicate());
events.extend(self.windows.replicate());
events.extend(self.keyboard_layouts.replicate());
events.extend(self.overview.replicate());
events.extend(self.config.replicate());
events.extend(self.casts.replicate());
events
}
@@ -109,9 +76,6 @@ impl EventStreamStatePart for EventStreamState {
let event = self.workspaces.apply(event)?;
let event = self.windows.apply(event)?;
let event = self.keyboard_layouts.apply(event)?;
let event = self.overview.apply(event)?;
let event = self.config.apply(event)?;
let event = self.casts.apply(event)?;
Some(event)
}
}
@@ -127,13 +91,6 @@ impl EventStreamStatePart for WorkspacesState {
Event::WorkspacesChanged { workspaces } => {
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
}
Event::WorkspaceUrgencyChanged { id, urgent } => {
for ws in self.workspaces.values_mut() {
if ws.id == id {
ws.is_urgent = urgent;
}
}
}
Event::WorkspaceActivated { id, focused } => {
let ws = self.workspaces.get(&id);
let ws = ws.expect("activated workspace was missing from the map");
@@ -205,32 +162,6 @@ impl EventStreamStatePart for WindowsState {
win.is_focused = Some(win.id) == id;
}
}
Event::WindowFocusTimestampChanged {
id,
focus_timestamp,
} => {
for win in self.windows.values_mut() {
if win.id == id {
win.focus_timestamp = focus_timestamp;
break;
}
}
}
Event::WindowUrgencyChanged { id, urgent } => {
for win in self.windows.values_mut() {
if win.id == id {
win.is_urgent = urgent;
break;
}
}
}
Event::WindowLayoutsChanged { changes } => {
for (id, update) in changes {
let win = self.windows.get_mut(&id);
let win = win.expect("changed window was missing from the map");
win.layout = update;
}
}
event => return Some(event),
}
None
@@ -261,63 +192,3 @@ impl EventStreamStatePart for KeyboardLayoutsState {
None
}
}
impl EventStreamStatePart for OverviewState {
fn replicate(&self) -> Vec<Event> {
vec![Event::OverviewOpenedOrClosed {
is_open: self.is_open,
}]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::OverviewOpenedOrClosed { is_open } => {
self.is_open = is_open;
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for ConfigState {
fn replicate(&self) -> Vec<Event> {
vec![Event::ConfigLoaded {
failed: self.failed,
}]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::ConfigLoaded { failed } => {
self.failed = failed;
}
event => return Some(event),
}
None
}
}
impl EventStreamStatePart for CastsState {
fn replicate(&self) -> Vec<Event> {
let casts = self.casts.values().cloned().collect();
vec![Event::CastsChanged { casts }]
}
fn apply(&mut self, event: Event) -> Option<Event> {
match event {
Event::CastsChanged { casts } => {
self.casts = casts.into_iter().map(|c| (c.stream_id, c)).collect();
}
Event::CastStartedOrChanged { cast } => {
self.casts.insert(cast.stream_id, cast);
}
Event::CastStopped { stream_id } => {
let cast = self.casts.remove(&stream_id);
cast.expect("stopped cast was missing from the map");
}
event => return Some(event),
}
None
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ edition.workspace = true
repository.workspace = true
[dependencies]
adw = { version = "0.8.1", package = "libadwaita", features = ["v1_4"] }
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_12"] }
niri = { version = "26.4.0", path = ".." }
niri-config = { version = "26.4.0", path = "../niri-config" }
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.2.0", path = ".." }
niri-config = { version = "25.2.0", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+7 -7
View File
@@ -3,7 +3,7 @@ use std::time::Duration;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
@@ -20,13 +20,11 @@ impl GradientArea {
pub fn new(_args: Args) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: 1.,
width: FloatOrInt(1.),
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
inactive_color: Color::default(),
urgent_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
});
Self {
@@ -83,14 +81,16 @@ impl TestCase for GradientArea {
g_size,
true,
true,
false,
Rectangle::default(),
CornerRadius::default(),
1.,
1.,
);
self.border
.render(renderer, g_loc, &mut |elem| rv.push(Box::new(elem) as _));
rv.extend(
self.border
.render(renderer, g_loc)
.map(|elem| Box::new(elem) as _),
);
rv.extend(
[BorderRenderElement::new(
+20 -31
View File
@@ -2,9 +2,9 @@ use std::collections::HashMap;
use std::time::Duration;
use niri::animation::Clock;
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options, SizingMode};
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::{Color, OutputName, PresetSize};
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
@@ -36,7 +36,6 @@ impl Layout {
subpixel: Subpixel::Unknown,
make: String::new(),
model: String::new(),
serial_number: String::new(),
},
);
let mode = Some(Mode {
@@ -52,27 +51,22 @@ impl Layout {
});
let options = Options {
layout: niri_config::Layout {
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: 4.,
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
active_gradient: None,
inactive_gradient: None,
urgent_gradient: None,
},
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: FloatOrInt(4.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
active_gradient: None,
inactive_gradient: None,
},
..Default::default()
};
let mut layout = niri::layout::Layout::with_options(clock.clone(), options);
layout.add_output(output.clone(), None);
layout.add_output(output.clone());
let start_time = clock.now_unadjusted();
@@ -168,7 +162,7 @@ impl Layout {
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
SizingMode::Normal,
false,
false,
None,
);
@@ -197,7 +191,7 @@ impl Layout {
let max_size = window.max_size();
window.request_size(
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
SizingMode::Normal,
false,
false,
None,
);
@@ -268,17 +262,12 @@ impl TestCase for Layout {
_size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
self.layout.update_render_elements(Some(&self.output));
let mut rv = Vec::new();
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_workspaces(ctx, true, &mut |elem| rv.push(Box::new(elem) as _));
rv
.render_elements(renderer, RenderTarget::Output, true)
.flat_map(|(_, iter)| iter)
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+14 -26
View File
@@ -2,9 +2,8 @@ use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::xray::XrayPos;
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::Color;
use niri::render_helpers::RenderTarget;
use niri_config::{Color, FloatOrInt};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Rectangle, Size};
@@ -59,17 +58,14 @@ impl Tile {
let Args { size, clock } = args;
let options = Options {
layout: niri_config::Layout {
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: 32.,
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
..Default::default()
},
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: FloatOrInt(32.),
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
..Default::default()
},
..Default::default()
@@ -119,19 +115,11 @@ impl TestCase for Tile {
self.tile.update_render_elements(
true,
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
1.,
);
let mut rv = Vec::new();
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
let xray_pos = XrayPos::new(location, 1.);
self.tile
.render(ctx, location, xray_pos, true, &mut |elem| {
rv.push(Box::new(elem) as _)
});
rv
.render(renderer, location, true, RenderTarget::Output)
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+16 -16
View File
@@ -1,5 +1,5 @@
use niri::layout::{LayoutElement, SizingMode};
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri::layout::LayoutElement;
use niri::render_helpers::RenderTarget;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Scale, Size};
@@ -14,14 +14,14 @@ pub struct Window {
impl Window {
pub fn freeform(args: Args) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(args.size, SizingMode::Normal, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(args.size, SizingMode::Normal, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
@@ -29,7 +29,7 @@ impl Window {
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
let mut window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(args.size, SizingMode::Normal, false, None);
window.request_size(args.size, false, false, None);
window.communicate();
Self { window }
}
@@ -38,7 +38,7 @@ impl Window {
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window
.request_size(Size::from((width, height)), SizingMode::Normal, false, None);
.request_size(Size::from((width, height)), false, false, None);
self.window.communicate();
}
@@ -52,16 +52,16 @@ impl TestCase for Window {
.to_f64()
.downscale(2.);
let mut rv = Vec::new();
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
self.window
.render_normal(ctx, location, Scale::from(1.), 1., &mut |elem| {
rv.push(Box::new(elem) as _)
});
rv
.render(
renderer,
location,
Scale::from(1.),
1.,
RenderTarget::Output,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+2 -11
View File
@@ -20,7 +20,6 @@ mod imp {
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
@@ -207,15 +206,8 @@ mod imp {
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
let cache = UserDataMap::new();
if element.is_framebuffer_effect() {
element
.capture_framebuffer(&mut frame, src, dst, &cache)
.context("error in capture_framebuffer()")?;
}
element
.draw(&mut frame, src, dst, &[damage], &[], Some(&cache))
.draw(&mut frame, src, dst, &[damage], &[])
.context("error drawing element")?;
}
}
@@ -263,8 +255,7 @@ mod imp {
glib::wrapper! {
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
@extends gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
@extends gtk::Widget;
}
impl SmithayView {
+39 -33
View File
@@ -4,12 +4,12 @@ use std::rc::Rc;
use niri::layout::{
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
LayoutElementRenderSnapshot, SizingMode,
LayoutElementRenderSnapshot,
};
use niri::render_helpers::offscreen::OffscreenData;
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::RenderCtx;
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::Kind;
@@ -24,7 +24,7 @@ struct TestWindowInner {
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
buffer: SolidColorBuffer,
pending_sizing_mode: SizingMode,
pending_fullscreen: bool,
csd_shadow_width: i32,
csd_shadow_buffer: SolidColorBuffer,
}
@@ -33,7 +33,6 @@ struct TestWindowInner {
pub struct TestWindow {
id: usize,
inner: Rc<RefCell<TestWindowInner>>,
rules: ResolvedWindowRules,
}
impl TestWindow {
@@ -51,11 +50,10 @@ impl TestWindow {
min_size,
max_size,
buffer,
pending_sizing_mode: SizingMode::Normal,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.3]),
})),
rules: ResolvedWindowRules::default(),
}
}
@@ -149,40 +147,47 @@ impl LayoutElement for TestWindow {
false
}
fn render_normal<R: NiriRenderer>(
fn render<R: NiriRenderer>(
&self,
_ctx: RenderCtx<R>,
_renderer: &mut R,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
) {
_target: RenderTarget,
) -> SplitElements<LayoutElementRenderElement<R>> {
let inner = self.inner.borrow();
push(
SolidColorRenderElement::from_buffer(&inner.buffer, location, alpha, Kind::Unspecified)
SplitElements {
normal: vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location,
alpha,
Kind::Unspecified,
)
.into(),
);
push(
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
alpha,
Kind::Unspecified,
)
.into(),
);
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
location
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
alpha,
Kind::Unspecified,
)
.into(),
],
popups: vec![],
}
}
fn request_size(
&mut self,
size: Size<i32, Logical>,
mode: SizingMode,
is_fullscreen: bool,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_sizing_mode = mode;
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
}
fn min_size(&self) -> Size<i32, Logical> {
@@ -227,12 +232,12 @@ impl LayoutElement for TestWindow {
fn send_pending_configure(&mut self) {}
fn pending_sizing_mode(&self) -> SizingMode {
self.inner.borrow().pending_sizing_mode
fn is_fullscreen(&self) -> bool {
false
}
fn sizing_mode(&self) -> SizingMode {
SizingMode::Normal
fn is_pending_fullscreen(&self) -> bool {
self.inner.borrow().pending_fullscreen
}
fn requested_size(&self) -> Option<Size<i32, Logical>> {
@@ -246,7 +251,12 @@ impl LayoutElement for TestWindow {
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
&self.rules
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
None
}
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
@@ -262,8 +272,4 @@ impl LayoutElement for TestWindow {
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
None
}
fn is_urgent(&self) -> bool {
false
}
}
+6 -30
View File
@@ -33,14 +33,10 @@ Summary: Scrollable-tiling Wayland compositor
SourceLicense: GPL-3.0-or-later
# (MIT OR Apache-2.0) AND BSD-3-Clause
# (MIT OR Apache-2.0) AND Unicode-3.0
# 0BSD OR MIT OR Apache-2.0
# Apache-2.0
# Apache-2.0 AND MIT
# Apache-2.0 OR BSL-1.0
# Apache-2.0 OR MIT
# Apache-2.0 OR MIT OR Unlicense
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
# BSD-2-Clause
# BSD-2-Clause OR Apache-2.0 OR MIT
@@ -48,19 +44,20 @@ SourceLicense: GPL-3.0-or-later
# GPL-3.0-or-later
# ISC
# MIT
# MIT AND (MIT OR Apache-2.0)
# MIT OR Apache-2.0
# MIT OR Apache-2.0 OR LGPL-2.1-or-later
# (MIT OR Apache-2.0) AND BSD-3-Clause
# (MIT OR Apache-2.0) AND Unicode-3.0
# MIT OR Apache-2.0 OR Zlib
# MIT OR Zlib OR Apache-2.0
# MPL-2.0
# Unicode-3.0
# Unlicense OR MIT
# Zlib
# Zlib OR Apache-2.0 OR MIT
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib) AND (Zlib OR Apache-2.0 OR MIT)
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
# LICENSE.dependencies contains a full license breakdown
URL: https://github.com/niri-wm/niri
URL: https://github.com/YaLTeR/niri
VCS: {{{ git_dir_vcs }}}
Source: {{{ git_dir_pack }}}
@@ -79,20 +76,10 @@ BuildRequires: pango-devel
BuildRequires: cairo-gobject-devel
# Needed for pipewire-rs
BuildRequires: clang
# Needed for some tests with a surfaceless EGL renderer
BuildRequires: mesa-libEGL
Requires: mesa-dri-drivers
Requires: mesa-libEGL
# Loaded through dlopen
Requires: libwayland-server
# Integrated Xwayland support. Not packaged on EPEL
%if 0%{?fedora}
Requires: xwayland-satellite >= 0.7
%endif
# Portal implementations used by niri
Recommends: xdg-desktop-portal-gtk
Recommends: xdg-desktop-portal-gnome
@@ -131,10 +118,6 @@ sed -i 's/\[env\]/[env]\nNIRI_BUILD_COMMIT="%{version}"/' .cargo/config.toml
%build
%cargo_build
target/rpm/niri completions bash > ./niri
target/rpm/niri completions fish > ./niri.fish
target/rpm/niri completions zsh > ./_niri
%install
%cargo_install
@@ -144,10 +127,6 @@ install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal ./resources/niri-po
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri.service
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
install -Dm644 -t %{buildroot}%{bash_completions_dir} ./niri
install -Dm644 -t %{buildroot}%{fish_completions_dir} ./niri.fish
install -Dm644 -t %{buildroot}%{zsh_completions_dir} ./_niri
%if %{with check}
%check
%cargo_test -- --workspace --exclude niri-visual-tests
@@ -157,7 +136,7 @@ install -Dm644 -t %{buildroot}%{zsh_completions_dir} ./_niri
%license LICENSE
%doc README.md
%doc resources/default-config.kdl
%doc docs/wiki
%doc wiki
%{_bindir}/niri
%{_bindir}/niri-session
%{_datadir}/wayland-sessions/niri.desktop
@@ -165,9 +144,6 @@ install -Dm644 -t %{buildroot}%{zsh_completions_dir} ./_niri
%{_datadir}/xdg-desktop-portal/niri-portals.conf
%{_userunitdir}/niri.service
%{_userunitdir}/niri-shutdown.target
%{bash_completions_dir}/niri
%{fish_completions_dir}/niri.fish
%{zsh_completions_dir}/_niri
%changelog
{{{ git_dir_changelog }}}
+22 -84
View File
@@ -1,11 +1,11 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://niri-wm.github.io/niri/Configuration:-Introduction
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
// Input device configuration.
// Find the full list of options on the wiki:
// https://niri-wm.github.io/niri/Configuration:-Input
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
@@ -15,19 +15,11 @@ input {
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
@@ -57,7 +49,6 @@ input {
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
}
@@ -73,7 +64,7 @@ input {
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Find more information on the wiki:
// https://niri-wm.github.io/niri/Configuration:-Outputs
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
// Uncomment this line to disable this output.
@@ -108,7 +99,7 @@ input {
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://niri-wm.github.io/niri/Configuration:-Layout
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
gaps 16
@@ -134,7 +125,7 @@ layout {
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Ctrl+Shift+R) toggles between.
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
@@ -170,9 +161,6 @@ layout {
active-color "#7fc8ff"
// Color of the ring on inactive monitors.
//
// The focus ring only draws around the active window, so the only place
// where you can see its inactive-color is on other monitors.
inactive-color "#505050"
// You can also use gradients. They take precedence over solid colors.
@@ -182,7 +170,7 @@ layout {
// You can use any CSS linear-gradient tool on the web to set these up.
// Changing the color space is also supported, check the wiki for more info.
//
// active-gradient from="#80c8ff" to="#c7ff7f" angle=45
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// You can also color the gradient relative to the entire view
// of the workspace, rather than relative to just the window itself.
@@ -201,14 +189,7 @@ layout {
active-color "#ffc87f"
inactive-color "#505050"
// Color of the border around windows that request your attention.
urgent-color "#9b0000"
// Gradients can use a few different interpolation color spaces.
// For example, this is a pastel rainbow gradient via in="oklch longer hue".
//
// active-gradient from="#e5989b" to="#ffb4a2" angle=45 relative-to="workspace-view" in="oklch longer hue"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
@@ -224,13 +205,13 @@ layout {
// radius. It has to assume that windows have square corners, leading to
// shadow artifacts inside the CSD rounded corners. This setting fixes
// those artifacts.
//
//
// However, instead you may want to set prefer-no-csd and/or
// geometry-corner-radius. Then, niri will know the corner radius and
// draw the shadow correctly, without having to draw it behind the
// window. These will also remove client-side shadows if the window
// draws any.
//
//
// draw-behind-window true
// You can change how shadows look. The values below are in logical
@@ -270,14 +251,6 @@ layout {
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "waybar"
// To run a shell command (with variables, pipes, etc.), use spawn-sh-at-startup:
// spawn-sh-at-startup "qs -c ~/source/qs/MyAwesomeShell"
hotkey-overlay {
// Uncomment this line to disable the "Important Hotkeys" pop-up at startup.
// skip-at-startup
}
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
@@ -295,7 +268,7 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
// Animation settings.
// The wiki explains how to configure individual animations:
// https://niri-wm.github.io/niri/Configuration:-Animations
// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations
animations {
// Uncomment to turn off all animations.
// off
@@ -306,7 +279,7 @@ animations {
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://niri-wm.github.io/niri/Configuration:-Window-Rules
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
@@ -366,39 +339,18 @@ binds {
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
// Use spawn-sh to run a shell command. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument. It's passed verbatim to `sh -c`.
// For example, this is a standard bind to toggle the screen reader (orca).
Super+Alt+S allow-when-locked=true hotkey-overlay-title=null { spawn-sh "pkill orca || exec orca"; }
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument in the end.
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
// Example volume keys mappings for PipeWire & WirePlumber.
// The allow-when-locked=true property makes them work even when the session is locked.
// Using spawn-sh allows to pass multiple arguments together with the command.
// "-l 1.0" limits the volume to 100%.
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1+ -l 1.0"; }
XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1-"; }
XF86AudioMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; }
XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
// Example media keys mapping using playerctl.
// This will work with any MPRIS-enabled media player.
XF86AudioPlay allow-when-locked=true { spawn-sh "playerctl play-pause"; }
XF86AudioStop allow-when-locked=true { spawn-sh "playerctl stop"; }
XF86AudioPrev allow-when-locked=true { spawn-sh "playerctl previous"; }
XF86AudioNext allow-when-locked=true { spawn-sh "playerctl next"; }
// Example brightness key mappings for brightnessctl.
// You can use regular spawn with multiple arguments too (to avoid going through "sh"),
// but you need to manually put each argument in separate "" quotes.
XF86MonBrightnessUp allow-when-locked=true { spawn "brightnessctl" "--class=backlight" "set" "+10%"; }
XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "--class=backlight" "set" "10%-"; }
// Open/close the Overview: a zoomed-out view of workspaces and windows.
// You can also move the mouse into the top-left hot corner,
// or do a four-finger swipe up on a touchpad.
Mod+O repeat=false { toggle-overview; }
Mod+Q repeat=false { close-window; }
Mod+Q { close-window; }
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
@@ -503,8 +455,8 @@ binds {
// These binds are also affected by touchpad's natural-scroll, so these
// example binds are "inverted", since we have natural-scroll enabled for
// touchpads by default.
// Mod+TouchpadScrollDown { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02+"; }
// Mod+TouchpadScrollUp { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02-"; }
// Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; }
// Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; }
// You can refer to workspaces by index. However, keep in mind that
// niri is a dynamic workspace system, so these commands are kind of
@@ -550,32 +502,18 @@ binds {
// Expel the bottom window from the focused column to the right.
Mod+Period { expel-window-from-column; }
// Cycle through widths set in preset-column-widths.
Mod+R { switch-preset-column-width; }
// Cycling through the presets in reverse order is also possible.
Mod+Shift+R { switch-preset-column-width-back; }
Mod+Ctrl+Shift+R { switch-preset-window-height; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
// While maximize-column leaves gaps and borders around the window,
// maximize-window-to-edges doesn't: the window expands to the edges of the screen.
// This bind corresponds to normal window maximizing,
// e.g. by double-clicking on the titlebar.
Mod+M { maximize-window-to-edges; }
// Expand the focused column to space not taken up by other fully visible columns.
// Makes the column "fill the rest of the space".
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
// Center all fully visible columns on screen.
Mod+Ctrl+C { center-visible-columns; }
// Finer width adjustments.
// This command can also:
// * set width in pixels: "1000"
+8 -7
View File
@@ -1,7 +1,8 @@
type = process
command = niri --session
restart = false
working-dir = $HOME
ready-notification = pipevar:NOTIFY_FD
logfile = $HOME/.local/share/niri/niri.log
depends-on: dbus
type = process
command = niri --session
restart = false
working-dir = $HOME
depends-on = dbus
after = niri-shutdown
chain-to = niri-shutdown
options: always-chain
+3
View File
@@ -0,0 +1,3 @@
type = scripted
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
restart = false
-6
View File
@@ -1,6 +0,0 @@
type = internal
restart = false
depends-on: niri
waits-for.d: $XDG_CONFIG_HOME/dinit.d/niri.d/
waits-for.d: $HOME/.config/dinit.d/niri.d/
waits-for.d: /etc/dinit.d/user/niri.d/
+3 -37
View File
@@ -1,15 +1,5 @@
#!/bin/sh
# Detect if being run as a user service, which implies external session management,
# exec compositor directly
if [ -n "${MANAGERPID:-}" ] && [ "${SYSTEMD_EXEC_PID:-}" = "$$" ]; then
case "$(ps -p "$MANAGERPID" -o cmd=)" in
*systemd*--user*)
exec niri --session
;;
esac
fi
if [ -n "$SHELL" ] &&
grep -q "$SHELL" /etc/shells &&
! (echo "$SHELL" | grep -q "false") &&
@@ -50,7 +40,7 @@ if hash systemctl >/dev/null 2>&1; then
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
elif hash dinitctl >/dev/null 2>&1; then
# Check that the user dinit daemon is running
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
@@ -59,37 +49,13 @@ elif hash dinitctl >/dev/null 2>&1; then
fi
# Make sure there's no already running session.
if dinitctl --quiet --user is-started niri 2>/dev/null; then
if dinitctl --user is-started niri >/dev/null 2>&1; then
echo 'A niri session is already running.'
exit 1
fi
# Import the login manager environment into dinit
# Might not work correctly for multiline variable names, but
# it is reasonable to assume there are none
awk 'BEGIN{for(v in ENVIRON) if (v != "AWKPATH" && v != "AWKLIBPATH") print v}' 2>/dev/null | xargs dinitctl --quiet --user setenv 2>/dev/null
# Usually the dbus service would start as niri's dependency and inherit
# environment from dinit, but in case it has already started we need
# to update its environment.
if hash dbus-update-activation-environment >/dev/null 2>&1; then
dbus-update-activation-environment --all >/dev/null 2>&1
fi
# Create the directory for the logfile, if doesn't exist
mkdir --parents $HOME/.local/share/niri
# Start niri
dinitctl --quiet --user start niri.target 2>&1
# Wait for termination
dinit-monitor --user --initial -c $'sh -c "
if [ "%s" = "stopped" ] || [ "%s" = "failed" ]; then
ppid=$(ps -o ppid= -p $$)
kill $ppid
fi"' niri >/dev/null 2>&1
# Unset environment that we've set.
dinitctl --quiet --user unsetenv WAYLAND_DISPLAY DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET 2>/dev/null
dinitctl --user start niri
else
echo "No systemd or dinit detected, please use niri --session instead."
fi
+1 -1
View File
@@ -11,4 +11,4 @@ Before=xdg-desktop-autostart.target
[Service]
Slice=session.slice
Type=notify
ExecStart=niri --session
ExecStart=/usr/bin/niri --session
-369
View File
@@ -1,369 +0,0 @@
use std::sync::mpsc;
use std::thread;
use accesskit::{
ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Live, Node, NodeId, Role,
Tree, TreeUpdate,
};
use accesskit_unix::Adapter;
use calloop::LoopHandle;
use niri_config::MruScope;
use crate::layout::workspace::WorkspaceId;
use crate::niri::{KeyboardFocus, Niri, State};
use crate::utils::with_toplevel_role;
use crate::window::mapped::MappedId;
const ID_ROOT: NodeId = NodeId(0);
const ID_ANNOUNCEMENT: NodeId = NodeId(1);
const ID_SCREENSHOT_UI: NodeId = NodeId(2);
const ID_EXIT_CONFIRM_DIALOG: NodeId = NodeId(3);
const ID_OVERVIEW: NodeId = NodeId(4);
const ID_MRU: NodeId = NodeId(5);
pub struct A11y {
event_loop: LoopHandle<'static, State>,
focus: NodeId,
workspace_id: Option<WorkspaceId>,
mru_selection: Option<MappedId>,
mru_scope: Option<MruScope>,
last_mru_title: String,
last_announcement: String,
to_accesskit: Option<mpsc::SyncSender<TreeUpdate>>,
}
enum Msg {
InitialTree,
Deactivate,
Action(ActionRequest),
}
impl A11y {
pub fn new(event_loop: LoopHandle<'static, State>) -> Self {
Self {
event_loop,
focus: ID_ROOT,
workspace_id: None,
mru_selection: None,
mru_scope: None,
last_mru_title: String::new(),
last_announcement: String::new(),
to_accesskit: None,
}
}
pub fn start(&mut self) {
let (tx, rx) = calloop::channel::channel();
let (to_accesskit, from_main) = mpsc::sync_channel::<TreeUpdate>(8);
// The adapter has a tendency to deadlock, so put it on a thread for now...
let handler = Handler { tx };
let res = thread::Builder::new()
.name("AccessKit Adapter".to_owned())
.spawn(move || {
let mut adapter = Adapter::new(handler.clone(), handler.clone(), handler);
while let Ok(tree) = from_main.recv() {
let is_focused = tree.focus != ID_ROOT;
adapter.update_if_active(move || tree);
adapter.update_window_focus_state(is_focused);
}
});
match res {
Ok(_handle) => {}
Err(err) => {
warn!("error spawning the AccessKit adapter thread: {err:?}");
return;
}
}
self.event_loop
.insert_source(rx, |e, _, state| match e {
calloop::channel::Event::Msg(msg) => state.niri.on_a11y_msg(msg),
calloop::channel::Event::Closed => (),
})
.unwrap();
self.to_accesskit = Some(to_accesskit);
}
fn update_tree(&mut self, tree: TreeUpdate) {
trace!("updating tree: {tree:?}");
self.focus = tree.focus;
let Some(tx) = &mut self.to_accesskit else {
return;
};
match tx.try_send(tree) {
Ok(()) => {}
Err(mpsc::TrySendError::Full(_)) => {
warn!("AccessKit channel is full, it probably deadlocked; disconnecting");
self.to_accesskit = None;
}
Err(mpsc::TrySendError::Disconnected(_)) => {
warn!("AccessKit channel disconnected");
self.to_accesskit = None;
}
}
}
}
impl Niri {
pub fn refresh_a11y(&mut self) {
if self.a11y.to_accesskit.is_none() {
return;
}
let _span = tracy_client::span!("refresh_a11y");
let mut announcement = None;
let ws_id = self.layout.active_workspace().map(|ws| ws.id());
if let Some(ws_id) = ws_id {
if self.a11y.workspace_id != Some(ws_id) {
let (_, idx, ws) = self
.layout
.workspaces()
.find(|(_, _, ws)| ws.id() == ws_id)
.unwrap();
let mut buf = format!("Workspace {}", idx + 1);
if let Some(name) = ws.name() {
buf.push(' ');
buf.push_str(name);
}
announcement = Some(buf);
}
}
self.a11y.workspace_id = ws_id;
let focus = self.a11y_focus();
// Check if the MRU selection changed.
let mut update_mru_selection = false;
if focus == ID_MRU {
let current = self.window_mru_ui.current_window_id();
if self.a11y.mru_selection != current {
update_mru_selection = true;
self.a11y.mru_selection = current;
}
// If there's no window title to announce, check if there's a scope change.
let scope = self.window_mru_ui.scope();
if !update_mru_selection && self.a11y.mru_scope != Some(scope) {
announcement = Some(self.window_mru_ui.a11y_scope_text());
}
self.a11y.mru_scope = Some(scope);
} else {
self.a11y.mru_scope = None;
self.a11y.mru_selection = None;
}
let update_focus = self.a11y.focus != focus;
if !(announcement.is_some() || update_focus || update_mru_selection) {
return;
}
let mut nodes = Vec::new();
if let Some(mut announcement) = announcement {
// Work around having to change node value for it to get announced.
if announcement == self.a11y.last_announcement {
announcement.push(' ');
}
self.a11y.last_announcement = announcement.clone();
let mut node = Node::new(Role::Label);
node.set_value(announcement);
node.set_live(Live::Polite);
nodes.push((ID_ANNOUNCEMENT, node));
}
if focus == ID_MRU {
// Ideally MRU would be a Group with a child Button for a window, but I've no idea how
// to make it work reliably. When I did it that way, there were two issues:
//
// 1. Alt-Tab would always start reading from "Recent windows grouping" instead of the
// window title.
// 2. When Alt-Tab became empty (e.g. switching scope to something empty), Orca would
// completely stop reading any child buttons for the remainder of the session.
//
// I've no idea what to do about these and where they even come from. So, just flip the
// MRU node between Group and Button, which seems to work fine.
if update_mru_selection {
if let Some(id) = self.a11y.mru_selection {
if let Some((_, mapped)) = self.layout.windows().find(|(_, m)| m.id() == id) {
with_toplevel_role(mapped.toplevel(), |role| {
let mut title = role.title.as_deref().unwrap_or("Unknown").to_owned();
// Change title on match to ensure we announce same-titled windows.
if self.a11y.last_mru_title == title {
title.push(' ');
}
self.a11y.last_mru_title = title;
let mut mru = Node::new(Role::Button);
mru.set_label(&*self.a11y.last_mru_title);
nodes.push((ID_MRU, mru));
});
}
} else {
let mut mru = Node::new(Role::Group);
// Announce the current scope in the empty text to make it clear.
let scope = self.window_mru_ui.a11y_scope_text();
mru.set_label(format!("Recent windows empty, {scope}"));
nodes.push((ID_MRU, mru));
}
}
}
let update = TreeUpdate {
nodes,
tree: None,
focus,
};
self.a11y.update_tree(update);
}
pub fn a11y_announce(&mut self, mut announcement: String) {
if self.a11y.to_accesskit.is_none() {
return;
}
let _span = tracy_client::span!("a11y_announce");
// Work around having to change node value for it to get announced.
if announcement == self.a11y.last_announcement {
announcement.push(' ');
}
self.a11y.last_announcement = announcement.clone();
let mut node = Node::new(Role::Label);
node.set_value(announcement);
node.set_live(Live::Polite);
let update = TreeUpdate {
nodes: vec![(ID_ANNOUNCEMENT, node)],
tree: None,
focus: self.a11y.focus,
};
self.a11y.update_tree(update);
}
pub fn a11y_announce_config_error(&mut self) {
if self.a11y.to_accesskit.is_none() {
return;
}
self.a11y_announce(crate::ui::config_error_notification::error_text(false));
}
pub fn a11y_announce_hotkey_overlay(&mut self) {
if self.a11y.to_accesskit.is_none() {
return;
}
self.a11y_announce(self.hotkey_overlay.a11y_text());
}
fn a11y_focus(&self) -> NodeId {
match self.keyboard_focus {
KeyboardFocus::ScreenshotUi => ID_SCREENSHOT_UI,
KeyboardFocus::ExitConfirmDialog => ID_EXIT_CONFIRM_DIALOG,
KeyboardFocus::Overview => ID_OVERVIEW,
KeyboardFocus::Mru => ID_MRU,
_ => ID_ROOT,
}
}
fn on_a11y_msg(&mut self, msg: Msg) {
match msg {
Msg::InitialTree => {
let tree = self.a11y_build_full_tree();
trace!("sending initial tree: {tree:?}");
self.a11y.update_tree(tree);
}
Msg::Deactivate => {
trace!("deactivate");
}
Msg::Action(request) => {
trace!("request: {request:?}");
}
}
}
fn a11y_build_full_tree(&self) -> TreeUpdate {
let mut node = Node::new(Role::Label);
node.set_live(Live::Polite);
let mut screenshot_ui = Node::new(Role::Group);
screenshot_ui.set_label("Screenshot UI");
let exit_confirm_dialog = crate::ui::exit_confirm_dialog::a11y_node();
let mut overview = Node::new(Role::Group);
overview.set_label("Overview");
let mut mru = Node::new(Role::Group);
mru.set_label("Recent windows");
let mut root = Node::new(Role::Window);
root.set_children(vec![
ID_ANNOUNCEMENT,
ID_SCREENSHOT_UI,
ID_EXIT_CONFIRM_DIALOG,
ID_OVERVIEW,
ID_MRU,
]);
let tree = Tree {
root: ID_ROOT,
toolkit_name: Some(String::from("niri")),
toolkit_version: None,
};
let focus = self.a11y_focus();
// NOTE: we don't fill in current MRU selection here to avoid duplicating code; it should
// get updated right away anyway.
TreeUpdate {
nodes: vec![
(ID_ROOT, root),
(ID_ANNOUNCEMENT, node),
(ID_SCREENSHOT_UI, screenshot_ui),
(ID_EXIT_CONFIRM_DIALOG, exit_confirm_dialog),
(ID_OVERVIEW, overview),
(ID_MRU, mru),
],
tree: Some(tree),
focus,
}
}
}
#[derive(Clone)]
struct Handler {
tx: calloop::channel::Sender<Msg>,
}
impl ActivationHandler for Handler {
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
let _ = self.tx.send(Msg::InitialTree);
None
}
}
impl DeactivationHandler for Handler {
fn deactivate_accessibility(&mut self) {
let _ = self.tx.send(Msg::Deactivate);
}
}
impl ActionHandler for Handler {
fn do_action(&mut self, request: ActionRequest) {
let _ = self.tx.send(Msg::Action(request));
}
}
-60
View File
@@ -1,60 +0,0 @@
use keyframe::EasingFunction;
#[derive(Debug, Clone, Copy)]
pub struct CubicBezier {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
}
impl CubicBezier {
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Self { x1, y1, x2, y2 }
}
// Based on libadwaita (LGPL-2.1-or-later):
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.7.6/src/adw-easing.c?ref_type=tags#L469-531
fn x_for_t(&self, t: f64) -> f64 {
let omt = 1. - t;
3. * omt * omt * t * self.x1 + 3. * omt * t * t * self.x2 + t * t * t
}
fn y_for_t(&self, t: f64) -> f64 {
let omt = 1. - t;
3. * omt * omt * t * self.y1 + 3. * omt * t * t * self.y2 + t * t * t
}
fn t_for_x(&self, x: f64) -> f64 {
let mut min_t = 0.;
let mut max_t = 1.;
for _ in 0..=30 {
let guess_t = (min_t + max_t) / 2.;
let guess_x = self.x_for_t(guess_t);
if x < guess_x {
max_t = guess_t;
} else {
min_t = guess_t;
}
}
(min_t + max_t) / 2.
}
}
impl EasingFunction for CubicBezier {
fn y(&self, x: f64) -> f64 {
if x <= f64::EPSILON {
return 0.;
}
if 1. - f64::EPSILON <= x {
return 1.;
}
self.y_for_t(self.t_for_x(x))
}
}
+8 -16
View File
@@ -3,9 +3,6 @@ use std::time::Duration;
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
use keyframe::EasingFunction;
mod bezier;
use bezier::CubicBezier;
mod spring;
pub use spring::{Spring, SpringParams};
@@ -46,7 +43,6 @@ pub enum Curve {
EaseOutQuad,
EaseOutCubic,
EaseOutExpo,
CubicBezier(CubicBezier),
}
impl Animation {
@@ -81,7 +77,7 @@ impl Animation {
let start_time = self.start_time;
match config.kind {
niri_config::animations::Kind::Spring(p) => {
niri_config::AnimationKind::Spring(p) => {
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
let spring = Spring {
@@ -92,7 +88,7 @@ impl Animation {
};
*self = Self::spring(self.clock.clone(), spring);
}
niri_config::animations::Kind::Easing(p) => {
niri_config::AnimationKind::Easing(p) => {
*self = Self::ease(
self.clock.clone(),
self.from,
@@ -346,21 +342,17 @@ impl Curve {
Curve::EaseOutQuad => EaseOutQuad.y(x),
Curve::EaseOutCubic => EaseOutCubic.y(x),
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
Curve::CubicBezier(b) => b.y(x),
}
}
}
impl From<niri_config::animations::Curve> for Curve {
fn from(value: niri_config::animations::Curve) -> Self {
impl From<niri_config::AnimationCurve> for Curve {
fn from(value: niri_config::AnimationCurve) -> Self {
match value {
niri_config::animations::Curve::Linear => Curve::Linear,
niri_config::animations::Curve::EaseOutQuad => Curve::EaseOutQuad,
niri_config::animations::Curve::EaseOutCubic => Curve::EaseOutCubic,
niri_config::animations::Curve::EaseOutExpo => Curve::EaseOutExpo,
niri_config::animations::Curve::CubicBezier(x1, y1, x2, y2) => {
Curve::CubicBezier(CubicBezier::new(x1, y1, x2, y2))
}
niri_config::AnimationCurve::Linear => Curve::Linear,
niri_config::AnimationCurve::EaseOutQuad => Curve::EaseOutQuad,
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
}
}
}
-19
View File
@@ -94,12 +94,6 @@ impl Spring {
x1 = (self.to - y0 + m * x0) / m;
y1 = self.oscillate(x1);
// Overdamped springs have some numerical stability issues...
if !y1.is_finite() {
return Duration::from_secs_f64(x0);
}
i += 1;
}
@@ -193,17 +187,4 @@ mod tests {
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
#[test]
fn overdamped_spring_duration_panic() {
let spring = Spring {
from: 0.,
to: 1.,
initial_velocity: 0.,
params: SpringParams::new(6., 1200., 0.0001),
};
let _ = spring.duration();
let _ = spring.clamped_duration();
let _ = spring.value_at(Duration::ZERO);
}
}
+3 -31
View File
@@ -1,16 +1,13 @@
//! Headless backend for tests.
//!
//! This can eventually grow into a more complete backend if needed, but for now it's missing some
//! crucial parts like dmabufs.
//! crucial parts like rendering.
use std::mem;
use std::sync::{Arc, Mutex};
use anyhow::Context as _;
use niri_config::OutputName;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::egl::native::EGLSurfacelessDisplay;
use smithay::backend::egl::{EGLContext, EGLDisplay};
use smithay::backend::renderer::element::RenderElementStates;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
@@ -20,44 +17,21 @@ use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState};
use crate::render_helpers::{resources, shaders};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Headless {
renderer: Option<GlesRenderer>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
impl Headless {
pub fn new() -> Self {
Self {
renderer: None,
ipc_outputs: Default::default(),
}
}
pub fn init(&mut self, _niri: &mut Niri) {}
pub fn add_renderer(&mut self) -> anyhow::Result<()> {
if self.renderer.is_some() {
error!("add_renderer: renderer must not already exist");
return Ok(());
}
let mut renderer = unsafe {
let display =
EGLDisplay::new(EGLSurfacelessDisplay).context("error creating EGL display")?;
let context = EGLContext::new(&display).context("error creating EGL context")?;
GlesRenderer::new(context).context("error creating renderer")?
};
resources::init(&mut renderer);
shaders::init(&mut renderer);
self.renderer = Some(renderer);
Ok(())
}
pub fn add_output(&mut self, niri: &mut Niri, n: u8, size: (u16, u16)) {
let connector = format!("headless-{n}");
let make = "niri".to_string();
@@ -71,7 +45,6 @@ impl Headless {
subpixel: Subpixel::Unknown,
make: make.clone(),
model: model.clone(),
serial_number: serial.clone(),
},
);
@@ -105,7 +78,6 @@ impl Headless {
is_preferred: true,
}],
current_mode: Some(0),
is_custom_mode: true,
vrr_supported: false,
vrr_enabled: false,
logical: Some(logical_output(&output)),
@@ -121,9 +93,9 @@ impl Headless {
pub fn with_primary_renderer<T>(
&mut self,
f: impl FnOnce(&mut GlesRenderer) -> T,
_f: impl FnOnce(&mut GlesRenderer) -> T,
) -> Option<T> {
self.renderer.as_mut().map(f)
None
}
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
-10
View File
@@ -20,7 +20,6 @@ pub use winit::Winit;
pub mod headless;
pub use headless::Headless;
#[allow(clippy::large_enum_variant)]
pub enum Backend {
Tty(Tty),
Winit(Winit),
@@ -56,7 +55,6 @@ impl OutputId {
impl Backend {
pub fn init(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Backend::init");
match self {
Backend::Tty(tty) => tty.init(niri),
Backend::Winit(winit) => winit.init(niri),
@@ -185,14 +183,6 @@ impl Backend {
}
}
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.update_ignored_nodes_config(niri),
Backend::Winit(_) => (),
Backend::Headless(_) => (),
}
}
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
+174 -1206
View File
File diff suppressed because it is too large Load Diff
+9 -58
View File
@@ -4,10 +4,8 @@ use std::mem;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use anyhow::Context as _;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::egl::EGLDevice;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -16,15 +14,13 @@ use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::platform::wayland::WindowAttributesExtWayland;
use smithay::reexports::winit::window::Window;
use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal};
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
@@ -32,7 +28,6 @@ pub struct Winit {
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
dmabuf_global: Option<DmabufGlobal>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
@@ -41,13 +36,10 @@ impl Winit {
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let _span = tracy_client::span!("Winit::new");
let builder = Window::default_attributes()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri")
.with_name("niri", "");
.with_title("niri");
let (backend, winit) = winit::init_from_attributes(builder)?;
let output = Output::new(
@@ -57,7 +49,6 @@ impl Winit {
subpixel: Subpixel::Unknown,
make: "Smithay".into(),
model: "Winit".into(),
serial_number: "Unknown".into(),
},
);
@@ -91,7 +82,6 @@ impl Winit {
is_preferred: true,
}],
current_mode: Some(0),
is_custom_mode: true,
vrr_supported: false,
vrr_enabled: false,
logical: Some(logical_output(&output)),
@@ -141,7 +131,6 @@ impl Winit {
output,
backend,
damage_tracker,
dmabuf_global: None,
ipc_outputs,
})
}
@@ -149,10 +138,7 @@ impl Winit {
pub fn init(&mut self, niri: &mut Niri) {
let renderer = self.backend.renderer();
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
// wl_drm is on its way out so this is expected on most modern distros.
trace!("error binding legacy EGL to wl_display: {err}");
} else {
debug!("bound legacy EGL to wl_display");
warn!("error binding renderer wl_display: {err}");
}
resources::init(renderer);
@@ -172,44 +158,9 @@ impl Winit {
niri.update_shaders();
self.create_dmabuf_global(niri);
niri.add_output(self.output.clone(), None, false);
}
pub fn create_dmabuf_global(&mut self, niri: &mut Niri) {
let renderer = self.backend.renderer();
let default_feedback = || {
let display = renderer.egl_context().display();
let device =
EGLDevice::device_for_display(display).context("error getting EGL device")?;
let node = device
.try_get_render_node()
.context("error getting EGL device render node")?
.context("failed to query EGL device render node")?;
let primary_formats = renderer.dmabuf_formats();
DmabufFeedbackBuilder::new(node.dev_id(), primary_formats)
.build()
.context("error building dmabuf feedback")
};
// Fallback to dmabuf v3 if we failed to build feedback.
let dmabuf_global = match default_feedback() {
Ok(feedback) => niri
.dmabuf_state
.create_global_with_default_feedback::<State>(&niri.display_handle, &feedback),
Err(err) => {
debug!("failed building default dmabuf feedback, falling back to v3: {err:?}");
let primary_formats = renderer.dmabuf_formats();
niri.dmabuf_state
.create_global::<State>(&niri.display_handle, primary_formats)
}
};
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
}
pub fn seat_name(&self) -> String {
"winit".to_owned()
}
@@ -225,12 +176,12 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let ctx = RenderCtx {
renderer: self.backend.renderer(),
target: RenderTarget::Output,
xray: None,
};
let mut elements = niri.render_to_vec(ctx, output, true);
let mut elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
+1 -30
View File
@@ -56,7 +56,7 @@ pub enum Sub {
/// Cause a panic to check if the backtraces are good.
Panic,
/// Generate shell completions.
Completions { shell: CompletionShell },
Completions { shell: Shell },
}
#[derive(Subcommand)]
@@ -105,33 +105,4 @@ pub enum Msg {
Version,
/// Request an error from the running niri instance.
RequestError,
/// Print the overview state.
OverviewState,
/// List screencasts.
Casts,
}
#[derive(Clone, Debug, clap::ValueEnum)]
pub enum CompletionShell {
Bash,
Elvish,
Fish,
PowerShell,
Zsh,
Nushell,
}
impl TryFrom<CompletionShell> for Shell {
type Error = &'static str;
fn try_from(shell: CompletionShell) -> Result<Self, Self::Error> {
match shell {
CompletionShell::Bash => Ok(Shell::Bash),
CompletionShell::Elvish => Ok(Shell::Elvish),
CompletionShell::Fish => Ok(Shell::Fish),
CompletionShell::PowerShell => Ok(Shell::PowerShell),
CompletionShell::Zsh => Ok(Shell::Zsh),
CompletionShell::Nushell => Err("Nushell should be handled separately"),
}
}
}
-483
View File
@@ -1,483 +0,0 @@
// References:
// - https://invent.kde.org/plasma/kwin/-/blob/397fbbe52a8f2d855ad0c9817b51a9bdf06a68e2/src/a11ykeyboardmonitor.cpp#L41
// - https://gitlab.gnome.org/GNOME/mutter/-/blob/cbb7295ac1f93a2dfd55a7c0544688e7e5c4d2e2/src/backends/meta-a11y-manager.c
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use anyhow::Context;
use futures_util::StreamExt;
use smithay::backend::input::{KeyState, Keycode};
use smithay::input::keyboard::{xkb, Keysym};
use zbus::blocking::object_server::InterfaceRef;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
use zbus::message::Header;
use zbus::names::{BusName, OwnedUniqueName, UniqueName};
use zbus::object_server::SignalEmitter;
use zbus::zvariant::NoneValue;
use super::Start;
use crate::niri::State;
#[derive(Debug, Default)]
struct Data {
clients: HashMap<OwnedUniqueName, Client>,
grabbed_mods: HashSet<Keysym>,
grabbed_mod_last_press_time: HashMap<Keysym, Duration>,
suppressed_keys: HashSet<Keysym>,
}
#[derive(Debug, Default)]
struct Client {
watched: bool,
grabbed: bool,
modifiers: HashSet<Keysym>,
keystrokes: Vec<(Keysym, u32)>,
}
#[derive(Clone)]
pub struct KeyboardMonitor {
data: Arc<Mutex<Data>>,
iface: Arc<OnceLock<InterfaceRef<Self>>>,
}
/// Keyboard monitor key block reason.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KbMonBlock {
/// Not blocked.
Pass,
/// Blocked, and this is the first press/release of the a11y modifier.
ModifierFirstPress,
/// Blocked, and this is not the a11y modifier.
Block,
}
/// Interface for monitoring of keyboard input by assistive technologies.
///
/// This interface is used by assistive technologies to monitor keyboard input of the compositor.
/// The compositor is expected to listen on the well-known bus name "org.freedesktop.a11y.Manager"
/// at the object path "/org/freedesktop/a11y/Manager".
#[interface(name = "org.freedesktop.a11y.KeyboardMonitor")]
impl KeyboardMonitor {
// Starts grabbing all key events. The client receives the events through the KeyEvent signal,
// and in addition, the events aren't handled normally by the compositor. This includes changes
// to the state of toggles like Caps Lock, Num Lock, and Scroll Lock.
//
// This behavior stays in effect until the same client calls UngrabKeyboard or closes its D-Bus
// connection.
async fn grab_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> {
let Some(sender) = hdr.sender() else {
return Err(fdo::Error::Failed("no sender".to_owned()));
};
let sender = OwnedUniqueName::from(sender.to_owned());
trace!("enabling keyboard grab for {sender}");
let mut data = self.data.lock().unwrap();
let client = data.clients.entry(sender).or_default();
client.grabbed = true;
Ok(())
}
// Reverses the effect of calling GrabKeyboard. If GrabKeyboard wasn't previously called, this
// method does nothing.
//
// After calling this method, the key grabs specified in the last call to SetKeyGrabs, if any,
// are still in effect. Also, the client will still receive key events through the KeyEvent
// signal, if it has called WatchKeyboard.
async fn ungrab_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> {
let Some(sender) = hdr.sender() else {
return Err(fdo::Error::Failed("no sender".to_owned()));
};
let sender = OwnedUniqueName::from(sender.to_owned());
let mut data = self.data.lock().unwrap();
if let Some(client) = data.clients.get_mut(&sender) {
trace!("disabling keyboard grab for {sender}");
client.grabbed = false;
}
Ok(())
}
// Starts watching all key events. The client receives the events through the KeyEvent signal,
// but the events are still handled normally by the compositor. This includes changes to the
// state of toggles like Caps Lock, Num Lock, and Scroll Lock.
//
// This behavior stays in effect until the same client calls UnwatchKeyboard or closes its D-Bus
// connection.
async fn watch_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> {
let Some(sender) = hdr.sender() else {
return Err(fdo::Error::Failed("no sender".to_owned()));
};
let sender = OwnedUniqueName::from(sender.to_owned());
trace!("enabling keyboard watch for {sender}");
let mut data = self.data.lock().unwrap();
let client = data.clients.entry(sender).or_default();
client.watched = true;
Ok(())
}
// Reverses the effect of calling WatchKeyboard. If WatchKeyboard wasn't previously called, this
// method does nothing.
//
// After calling this method, the key grabs specified in the last call to SetKeyGrabs, if any,
// are still in effect, but other key events are no longer reported to this client.
async fn unwatch_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> {
let Some(sender) = hdr.sender() else {
return Err(fdo::Error::Failed("no sender".to_owned()));
};
let sender = OwnedUniqueName::from(sender.to_owned());
let mut data = self.data.lock().unwrap();
if let Some(client) = data.clients.get_mut(&sender) {
trace!("disabling keyboard watch for {sender}");
client.watched = false;
}
Ok(())
}
// Sets the current key grabs for the calling client, overriding any previous call to this
// method. For grabbed key events, the KeyEvent signal is emitted, and normal key event handling
// is suppressed, including state changes for toggles like Caps Lock and Num Lock.
//
// The grabs set by this method stay in effect until the same client calls this method again, or
// until that client closes its D-Bus connection.
//
// Each item in `modifiers` is an XKB keysym. All keys in this list will be grabbed, and keys
// pressed while any of these keys are down will also be grabbed.
//
// Each item in `keystrokes` is a struct with the following fields:
//
// - the XKB keysym of the non-modifier key
// - the XKB modifier mask of the modifiers, if any, for this keystroke
//
// If any of the keys in `modifiers` is pressed alone, the compositor is required to ignore the
// key press and release event if a second key press of the same modifier is not received within
// a reasonable time frame, for example, the key repeat delay. If such event is received, this
// second event is processed normally.
async fn set_key_grabs(
&self,
#[zbus(header)] hdr: Header<'_>,
modifiers: Vec<u32>,
keystrokes: Vec<(u32, u32)>,
) -> fdo::Result<()> {
let Some(sender) = hdr.sender() else {
return Err(fdo::Error::Failed("no sender".to_owned()));
};
let sender = OwnedUniqueName::from(sender.to_owned());
trace!("updating key grabs for {sender}");
let mut data = self.data.lock().unwrap();
let client = data.clients.entry(sender).or_default();
client.modifiers = HashSet::from_iter(modifiers.into_iter().map(Keysym::new));
client.keystrokes =
Vec::from_iter(keystrokes.into_iter().map(|(k, v)| (Keysym::new(k), v)));
data.rebuild_grabbed_mods();
Ok(())
}
// The compositor emits this signal for each key press or release.
//
// - `released`: whether this is a key-up event
// - `state`: XKB modifier mask for currently pressed modifiers
// - `keysym`: XKB keysym for this key
// - `unichar`: Unicode character for this key, or 0 if none
// - `keycode`: hardware-dependent keycode for this key
#[zbus(signal)]
pub async fn key_event(
ctxt: &SignalEmitter<'_>,
released: bool,
state: u32,
keysym: u32,
unichar: u32,
keycode: u16,
) -> zbus::Result<()>;
}
impl KeyboardMonitor {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
data: Arc::new(Mutex::new(Data::default())),
iface: Arc::new(OnceLock::new()),
}
}
#[allow(clippy::too_many_arguments)]
pub fn process_key(
&self,
repeat_delay: Duration,
time: Duration,
keycode: Keycode,
released: bool,
mods: u32,
keysym: Keysym,
unichar: u32,
) -> KbMonBlock {
let _span = tracy_client::span!("KeyboardMonitor::process_key");
let mut ctxt = self.iface.get().unwrap().signal_emitter().clone();
let mut data = self.data.lock().unwrap();
// Emit key events as necessary.
for (name, client) in &data.clients {
if client.should_watch_keypress(&data.suppressed_keys, mods, keysym) {
let _span = tracy_client::span!("emitting key event");
// Emit to that client only.
ctxt = ctxt.set_destination(BusName::Unique(name.as_ref()));
let ctxt = &ctxt;
async_io::block_on(async move {
if let Err(err) = KeyboardMonitor::key_event(
ctxt,
released,
mods,
keysym.raw(),
unichar,
keycode.raw() as u16,
)
.await
{
warn!("error emitting key_event: {err:?}");
}
});
}
}
// Check for double-pressed grabbed modifier that should not be captured.
if data.grabbed_mods.contains(&keysym) {
if released {
// If missing from suppressed keys, then this is a release corresponding to the
// second press that got handled normally.
if !data.suppressed_keys.contains(&keysym) {
trace!("handling release for second press of grabbed modifier: {keysym:?}");
return KbMonBlock::Pass;
}
} else {
let last_press_entry = data
.grabbed_mod_last_press_time
.entry(keysym)
.or_insert(Duration::ZERO);
let last_press = *last_press_entry;
*last_press_entry = time;
// Modifier pressed twice; handle it as normal.
if time <= last_press.saturating_add(repeat_delay) {
trace!("handling second press of grabbed modifier: {keysym:?}");
return KbMonBlock::Pass;
}
}
}
let mut block = false;
if released {
// This is a release for a key that was grabbed.
if data.suppressed_keys.remove(&keysym) {
trace!("blocking release for previously suppressed key: {keysym:?}");
block = true;
}
} else if data.suppressed_keys.contains(&keysym) {
// Second press for an already-pressed key, e.g. from two keyboards.
trace!("blocking press for already-pressed key: {keysym:?}");
block = true;
} else {
// Check if it's grabbed by any client.
if data
.clients
.values()
.any(|client| client.should_grab_keypress(&data.suppressed_keys, mods, keysym))
{
trace!("blocking press for grabbed key: {keysym:?}");
data.suppressed_keys.insert(keysym);
block = true;
}
}
if !block {
KbMonBlock::Pass
} else if data.grabbed_mods.contains(&keysym) {
KbMonBlock::ModifierFirstPress
} else {
KbMonBlock::Block
}
}
}
impl Data {
fn rebuild_grabbed_mods(&mut self) {
self.grabbed_mods.clear();
for client in self.clients.values() {
self.grabbed_mods.extend(&client.modifiers);
}
}
}
impl Client {
fn should_grab_keypress(
&self,
suppressed_keys: &HashSet<Keysym>,
mods: u32,
keysym: Keysym,
) -> bool {
// Grabbing all keys.
if self.grabbed {
return true;
}
for modifier in &self.modifiers {
// This is a grabbed modifier, or a grabbed modifier is currently down.
if *modifier == keysym || suppressed_keys.contains(modifier) {
return true;
}
}
for (grabbed_keysym, grabbed_mods) in &self.keystrokes {
// This is a grabbed keystroke.
if *grabbed_keysym == keysym && *grabbed_mods == mods {
return true;
}
}
false
}
fn should_watch_keypress(
&self,
suppressed_keys: &HashSet<Keysym>,
mods: u32,
keysym: Keysym,
) -> bool {
if self.watched {
return true;
}
self.should_grab_keypress(suppressed_keys, mods, keysym)
}
}
async fn monitor_disappeared_clients(
conn: &zbus::Connection,
data: Arc<Mutex<Data>>,
) -> anyhow::Result<()> {
let proxy = fdo::DBusProxy::new(conn)
.await
.context("error creating a DBusProxy")?;
let mut stream = proxy
.receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())])
.await
.context("error creating a NameOwnerChanged stream")?;
while let Some(signal) = stream.next().await {
let args = signal
.args()
.context("error retrieving NameOwnerChanged args")?;
let Some(name) = &**args.old_owner() else {
continue;
};
if args.new_owner().is_none() {
trace!("keyboard monitor client disconnected: {name}");
let name = OwnedUniqueName::from(name.to_owned());
let mut data = data.lock().unwrap();
data.clients.remove(&name);
data.rebuild_grabbed_mods();
} else {
error!("non-null new_owner should've been filtered out");
}
}
Ok(())
}
impl Start for KeyboardMonitor {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let data = self.data.clone();
let conn = zbus::blocking::Connection::session()?;
let flags = RequestNameFlags::AllowReplacement
| RequestNameFlags::ReplaceExisting
| RequestNameFlags::DoNotQueue;
conn.object_server()
.at("/org/freedesktop/a11y/Manager", self.clone())?;
conn.request_name_with_flags("org.freedesktop.a11y.Manager", flags)?;
let iface = conn
.object_server()
.interface("/org/freedesktop/a11y/Manager")?;
let _ = self.iface.set(iface);
let async_conn = conn.inner().clone();
let future = async move {
if let Err(err) = monitor_disappeared_clients(&async_conn, data.clone()).await {
warn!("error monitoring keyboard monitor clients: {err:?}");
// Since the monitor is now broken, prevent any further communication.
if let Err(err) = async_conn.close().await {
warn!("error closing connection: {err:?}");
}
let mut data = data.lock().unwrap();
data.clients.clear();
data.rebuild_grabbed_mods();
}
};
let task = conn
.inner()
.executor()
.spawn(future, "monitor disappearing keyboard clients");
task.detach();
Ok(conn)
}
}
impl State {
pub fn a11y_process_key(
&mut self,
time: Duration,
keycode: Keycode,
state: KeyState,
) -> KbMonBlock {
if self.niri.a11y_keyboard_monitor.is_none() {
return KbMonBlock::Pass;
}
let keyboard = self.niri.seat.get_keyboard().unwrap();
let (mods, keysym, unichar) = keyboard.with_xkb_state(self, |context| {
let xkb = context.xkb().lock().unwrap();
// SAFETY: we're not changing the ref count.
let state = unsafe { xkb.state() };
let keysym = state.key_get_one_sym(keycode);
let mods = state.serialize_mods(xkb::STATE_MODS_EFFECTIVE);
let unichar = state.key_get_utf32(keycode);
(mods, keysym, unichar)
});
let config = self.niri.config.borrow();
let repeat_delay = Duration::from_millis(u64::from(config.input.keyboard.repeat_delay));
let released = state == KeyState::Released;
let Some(monitor) = &self.niri.a11y_keyboard_monitor else {
return KbMonBlock::Pass;
};
monitor.process_key(repeat_delay, time, keycode, released, mods, keysym, unichar)
}
}
-144
View File
@@ -1,144 +0,0 @@
use futures_util::StreamExt;
use niri_config::Xkb;
use zbus::names::InterfaceName;
use zbus::{fdo, zvariant};
pub enum Locale1ToNiri {
XkbChanged(Xkb),
}
pub fn start(
to_niri: calloop::channel::Sender<Locale1ToNiri>,
) -> anyhow::Result<zbus::blocking::Connection> {
let conn = zbus::blocking::Connection::system()?;
let async_conn = conn.inner().clone();
let future = async move {
let proxy = fdo::PropertiesProxy::new(
&async_conn,
"org.freedesktop.locale1",
"/org/freedesktop/locale1",
)
.await;
let proxy = match proxy {
Ok(x) => x,
Err(err) => {
warn!("error creating PropertiesProxy: {err:?}");
return;
}
};
let mut props_changed = match proxy.receive_properties_changed().await {
Ok(x) => x,
Err(err) => {
warn!("error subscribing to PropertiesChanged: {err:?}");
return;
}
};
let props = proxy
.get_all(InterfaceName::try_from("org.freedesktop.locale1").unwrap())
.await;
let mut props = match props {
Ok(x) => x,
Err(err) => {
warn!("error receiving initial properties: {err:?}");
return;
}
};
trace!("initial properties: {props:?}");
let mut get = |name| {
props
.remove(name)
.and_then(|x| String::try_from(x).ok())
.unwrap_or_default()
};
let mut xkb = Xkb {
rules: String::new(),
model: get("X11Model"),
layout: get("X11Layout"),
variant: get("X11Variant"),
options: match get("X11Options") {
x if x.is_empty() => None,
x => Some(x),
},
file: None,
};
// Send the initial properties.
if let Err(err) = to_niri.send(Locale1ToNiri::XkbChanged(xkb.clone())) {
warn!("error sending message to niri: {err:?}");
return;
};
while let Some(changed) = props_changed.next().await {
let args = match changed.args() {
Ok(args) => args,
Err(err) => {
warn!("error parsing locale1 PropertiesChanged args: {err:?}");
return;
}
};
let mut changed = false;
for (name, value) in args.changed_properties() {
trace!("changed property: {name} => {value:?}");
let value = zvariant::Str::try_from(value).unwrap_or_default();
let value = value.as_str();
match *name {
"X11Model" => {
if xkb.model != value {
xkb.model = String::from(value);
changed = true;
}
}
"X11Layout" => {
if xkb.layout != value {
xkb.layout = String::from(value);
changed = true;
}
}
"X11Variant" => {
if xkb.variant != value {
xkb.variant = String::from(value);
changed = true;
}
}
"X11Options" => {
let value = match value {
"" => None,
x => Some(x),
};
if xkb.options.as_deref() != value {
xkb.options = value.map(String::from);
changed = true;
}
}
_ => (),
}
}
if !changed {
continue;
}
if let Err(err) = to_niri.send(Locale1ToNiri::XkbChanged(xkb.clone())) {
warn!("error sending message to niri: {err:?}");
return;
};
}
};
let task = conn
.inner()
.executor()
.spawn(future, "monitor locale1 property changes");
task.detach();
Ok(conn)
}
-105
View File
@@ -1,105 +0,0 @@
use futures_util::StreamExt;
use zbus::fdo;
use zbus::names::InterfaceName;
pub enum Login1ToNiri {
LidClosedChanged(bool),
}
pub fn start(
to_niri: calloop::channel::Sender<Login1ToNiri>,
) -> anyhow::Result<zbus::blocking::Connection> {
let conn = zbus::blocking::Connection::system()?;
let async_conn = conn.inner().clone();
let future = async move {
let proxy = fdo::PropertiesProxy::new(
&async_conn,
"org.freedesktop.login1",
"/org/freedesktop/login1",
)
.await;
let proxy = match proxy {
Ok(x) => x,
Err(err) => {
warn!("error creating PropertiesProxy: {err:?}");
return;
}
};
let mut props_changed = match proxy.receive_properties_changed().await {
Ok(x) => x,
Err(err) => {
warn!("error subscribing to PropertiesChanged: {err:?}");
return;
}
};
let props = proxy
.get_all(InterfaceName::try_from("org.freedesktop.login1.Manager").unwrap())
.await;
let mut props = match props {
Ok(x) => x,
Err(err) => {
warn!("error receiving initial properties: {err:?}");
return;
}
};
trace!("initial properties: {props:?}");
let mut lid_closed = props
.remove("LidClosed")
.and_then(|value| bool::try_from(value).ok())
.unwrap_or_default();
if let Err(err) = to_niri.send(Login1ToNiri::LidClosedChanged(lid_closed)) {
warn!("error sending initial lid state to niri: {err:?}");
return;
};
while let Some(signal) = props_changed.next().await {
let args = match signal.args() {
Ok(args) => args,
Err(err) => {
warn!("error parsing PropertiesChanged args: {err:?}");
return;
}
};
let mut new_lid_closed = lid_closed;
let mut changed = false;
for (name, value) in args.changed_properties() {
trace!("changed property: {name} => {value:?}");
if *name != "LidClosed" {
continue;
}
new_lid_closed = bool::try_from(value).unwrap_or(new_lid_closed);
changed = true;
}
if !changed {
continue;
}
if new_lid_closed == lid_closed {
continue;
}
lid_closed = new_lid_closed;
if let Err(err) = to_niri.send(Login1ToNiri::LidClosedChanged(lid_closed)) {
warn!("error sending message to niri: {err:?}");
return;
};
}
};
let task = conn
.inner()
.executor()
.spawn(future, "monitor login1 property changes");
task.detach();
Ok(conn)
}

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