Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49fc6117fd | |||
| 165a6d1ee8 | |||
| fdb6d85fc7 | |||
| 188c5300f7 | |||
| a4b5539baa | |||
| 6f1a2c5f0e | |||
| f717ae030f | |||
| f3696081d1 | |||
| 4b60cbe537 | |||
| f9f43d826a | |||
| 3d49db3870 | |||
| 9bd6c2cadd | |||
| c5253968b4 | |||
| 9a6f31012d | |||
| 4294948cf1 | |||
| cd5ac3e5e0 | |||
| 38191826cb | |||
| 0200670d9e | |||
| 90366886b2 | |||
| 56654034e9 | |||
| 1f07cffa9f | |||
| cb3a06cd54 | |||
| f115f2e5e7 | |||
| 2e07282977 | |||
| cba0454c94 | |||
| 5f6f131b24 | |||
| adb5b3cd2c | |||
| 0650e7b640 | |||
| dd1c3bcb9f | |||
| e5d463e15b | |||
| 26100096e8 | |||
| a85b922919 | |||
| 7d2b620ce9 | |||
| a48f2645d9 | |||
| 83e839762f | |||
| 4c1196f45b | |||
| 8ed0da44d9 | |||
| 91be662ac6 | |||
| 4438aefc8d | |||
| 6c4dfd7772 | |||
| 1ad422f0db | |||
| 8fd9fb73f2 | |||
| 8d83fbae67 | |||
| 414729dce5 | |||
| dbe79b7873 | |||
| 9438f59e2b | |||
| 719255ac35 | |||
| 8a51935224 | |||
| 8d583fe854 | |||
| 74d2b18603 | |||
| fad02316f1 | |||
| 47385c2ecd | |||
| e430d3ab2b | |||
| e472b5b0f1 | |||
| efb169416d | |||
| 3a3a97ec2a | |||
| e9c182a13c | |||
| cfe059c303 | |||
| bd7c748a4f | |||
| 68bb942d21 | |||
| 04c422e43f | |||
| d09fa2709c | |||
| 2c3315aebb | |||
| 5a45088061 | |||
| 404d6dccc4 | |||
| 084f2cb193 | |||
| 25c88b542f | |||
| 6fc50a1fb8 | |||
| 5e11b96f12 | |||
| 849d26d646 | |||
| 9e5716a9db | |||
| f4ebbc8017 | |||
| ce9dd33213 | |||
| 10995ec62c | |||
| c814c656c5 | |||
| 82d4c7569e | |||
| 4f0db78248 | |||
| 3e250cdc12 | |||
| a1b0bd6d1c | |||
| 892470afd3 | |||
| f1cb02cfab | |||
| 5dc4e83ba7 | |||
| 2b58e03d30 | |||
| d4b4407236 | |||
| 26ff5f4bf1 | |||
| d7905e6b74 | |||
| 71d7fa9a61 | |||
| 707f08559c | |||
| 4d21489101 | |||
| 5a24aae560 | |||
| 9170161a0a | |||
| 66d66d6030 | |||
| 19866f8b0b | |||
| 4bc3ede4b7 | |||
| 250aa1f3cb | |||
| 0117d6953d | |||
| 931123f38c | |||
| 73c0ce75d8 | |||
| 1b1715fe9b | |||
| fee8719299 | |||
| 7f9c7d1415 | |||
| b81cb13c2c | |||
| 45582ad095 | |||
| b3f5255bb9 | |||
| 8c169b1a14 | |||
| 525b33777b | |||
| dec0e3bf5a | |||
| 6bcaaf9d21 | |||
| f022b3c504 | |||
| ab10a260fa | |||
| 0eddd16b8a | |||
| d020d986ed | |||
| 5abeb923de | |||
| dd1f28998f | |||
| 874e7fd70e | |||
| 599db847f8 | |||
| d1a0380eed | |||
| 8f48f56fe1 | |||
| b07bde3ee8 | |||
| bf142e0b48 | |||
| 8f75d171b6 | |||
| cbf4631461 | |||
| a217ad6424 | |||
| f4dc10e0b4 | |||
| b82d52705e | |||
| c7fa5f29d6 | |||
| e708f54615 | |||
| 2dc6f4482c | |||
| a2a5291175 | |||
| 1fa0338a17 | |||
| 8e3e93b624 | |||
| c1146c0bef | |||
| 41b5de8769 | |||
| 8d9bc2a5c9 | |||
| 6d5c5f12b2 | |||
| 42b2aeb6e6 | |||
| ab47f5cec4 | |||
| 549148d277 | |||
| 189917c933 |
@@ -1,12 +0,0 @@
|
||||
# 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'
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Report a bug or a crash
|
||||
title: ''
|
||||
labels: bug
|
||||
type: Bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
contact_links:
|
||||
- name: Feature request
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
||||
url: https://github.com/niri-wm/niri/discussions/new?category=ideas
|
||||
about: Ideas for new features and functionality (start a Discussion)
|
||||
- name: Ask a question
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
|
||||
url: https://github.com/niri-wm/niri/discussions/new?category=q-a
|
||||
about: Question about niri (start a Discussion)
|
||||
- name: Matrix room
|
||||
url: https://matrix.to/#/#niri:matrix.org
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
container: alpine:3
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
PROPTEST_MAX_SHRINK_ITERS: 200000
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -217,7 +217,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -230,10 +230,10 @@ jobs:
|
||||
|
||||
fedora:
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:41
|
||||
container: fedora:42
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
CARGO_HOME: /home/runner/work/niri/niri/cargo-home
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
dotnet: false
|
||||
large-packages: false
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -308,7 +308,7 @@ jobs:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
|
||||
@@ -22,15 +22,18 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check for unreplaced "Since:" in the wiki
|
||||
run: |
|
||||
if grep --recursive 'Since: next release' wiki; then
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
/target
|
||||
/result
|
||||
|
||||
.idea
|
||||
@@ -31,7 +31,7 @@ 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://yalter.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
|
||||
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.
|
||||
@@ -84,7 +84,7 @@ When creating pull requests, please keep the following in mind.
|
||||
- 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://yalter.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
|
||||
- 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.
|
||||
|
||||
@@ -6,37 +6,35 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "25.11.0"
|
||||
version = "26.4.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/YaLTeR/niri"
|
||||
repository = "https://github.com/niri-wm/niri"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
bitflags = "2.10.0"
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
insta = "1.46.0"
|
||||
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"] }
|
||||
# 0.3.20 filters out all ANSI codes to "fix a security issue" while also breaking
|
||||
# everyone who relied on them for color output, with no fallback available.
|
||||
# https://github.com/tokio-rs/tracing/issues/3378
|
||||
tracing-subscriber = { version = "=0.3.19", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.18.4", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
# version = "0.4.1"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay"
|
||||
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
|
||||
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]
|
||||
@@ -53,49 +51,50 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { version = "0.22.0", optional = true }
|
||||
accesskit_unix = { version = "0.18.0", optional = true }
|
||||
accesskit = { version = "0.24", optional = true }
|
||||
accesskit_unix = { version = "0.22", 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"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.24.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.3", features = ["executor", "futures-io", "signals"] }
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.4", features = ["executor", "futures-io", "signals"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
clap_complete = "4.5.65"
|
||||
clap_complete_nushell = "4.5.10"
|
||||
clap_complete = "4.6.2"
|
||||
clap_complete_nushell = "4.6.0"
|
||||
directories = "6.0.0"
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.3.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
drm-ffi = "0.9.1"
|
||||
fastrand = "2.4.1"
|
||||
futures-util = { version = "0.3.32", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.30.10"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
glam = "0.32.1"
|
||||
input = { version = "0.10.0", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.180"
|
||||
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 = "25.11.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.11.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.1.0"
|
||||
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.0"
|
||||
png = "0.18.1"
|
||||
profiling = "1.0.17"
|
||||
sd-notify = "0.4.5"
|
||||
sd-notify = "0.5.0"
|
||||
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.12"
|
||||
wayland-scanner = "0.31.8"
|
||||
wayland-backend = "0.3.15"
|
||||
wayland-scanner = "0.31.10"
|
||||
wayland-server = "0.31.13"
|
||||
xcursor = "0.3.10"
|
||||
zbus = { version = "5.13.0", optional = true }
|
||||
zbus = { version = "5.13.2", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -119,14 +118,14 @@ features = [
|
||||
approx = "0.5.1"
|
||||
calloop-wayland-source = "0.4.1"
|
||||
insta.workspace = true
|
||||
proptest = "1.9.0"
|
||||
proptest-derive = { version = "0.7.0", features = ["boxed_union"] }
|
||||
rayon = "1.11.0"
|
||||
wayland-client = "0.31.12"
|
||||
proptest = "1.11.0"
|
||||
proptest-derive = { version = "0.8.0", features = ["boxed_union"] }
|
||||
rayon = "1.12.0"
|
||||
wayland-client = "0.31.14"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3.32"
|
||||
pkg-config = "0.3.33"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
@@ -147,6 +146,7 @@ dinit = []
|
||||
|
||||
[lints.clippy]
|
||||
new_without_default = "allow"
|
||||
collapsible_match = "allow"
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
@@ -162,7 +162,7 @@ insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "25.11"
|
||||
version = "26.04"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<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/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>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://yalter.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://yalter.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup Showcase</a>
|
||||
<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 Showcase</a>
|
||||
</p>
|
||||
|
||||

|
||||
<img width="1280" height="720" alt="niri with a few windows open" src="https://github.com/user-attachments/assets/dea5909e-1859-4aaa-9d88-d37f9663e00b" />
|
||||
|
||||
## About
|
||||
|
||||
@@ -29,25 +29,29 @@ 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://yalter.github.io/niri/Workspaces.html) like in GNOME
|
||||
- [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
|
||||
- Built-in screenshot UI
|
||||
- Monitor and window screencasting through xdg-desktop-portal-gnome
|
||||
- You can [block out](https://yalter.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
|
||||
- [Dynamic cast target](https://yalter.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
|
||||
- [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://yalter.github.io/niri/Tabs.html)
|
||||
- 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)
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- [Gradient borders](https://yalter.github.io/niri/Configuration%3A-Layout.html#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)
|
||||
- [Gradient borders](https://niri-wm.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
|
||||
- [Background blur](https://niri-wm.github.io/niri/Window-Effects.html) for windows and layer-shell surfaces
|
||||
- [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)
|
||||
- Live-reloading config
|
||||
- Works with [screen readers](https://yalter.github.io/niri/Accessibility.html)
|
||||
- Works with [screen readers](https://niri-wm.github.io/niri/Accessibility.html)
|
||||
|
||||
## Video Demo
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
|
||||
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)
|
||||
Also check out these videos that showcase a lot of the niri functionality:
|
||||
|
||||
- [Niri Is My New Favorite Wayland Compositor](https://www.youtube.com/watch?v=DeYx2exm04M) by Brodie Robertson
|
||||
- [How Is niri This Good? Live Demo + Config](https://www.youtube.com/watch?v=7XmD5UyyhZQ) by Nick Janetakis
|
||||
|
||||
## Status
|
||||
|
||||
@@ -55,8 +59,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://yalter.github.io/niri/Getting-Started.html) page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
Follow the instructions on the [Getting Started](https://niri-wm.github.io/niri/Getting-Started.html) page.
|
||||
Grab a desktop shell like [DankMaterialShell] or [Noctalia] (or build a more traditional setup): niri by itself 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,7 +76,7 @@ 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://yalter.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
|
||||
- **Xwayland**: [integrated](https://niri-wm.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
|
||||
|
||||
## Media
|
||||
|
||||
@@ -93,7 +97,7 @@ 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/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
|
||||
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
|
||||
|
||||
## Inspiration
|
||||
|
||||
@@ -109,8 +113,8 @@ 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.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
- Hyprland has a built-in [scrolling layout](https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/).
|
||||
- [Paneru] and [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Contact
|
||||
|
||||
@@ -121,11 +125,12 @@ We also have a community Discord server: https://discord.gg/vT8Sfjy7sx
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[waybar]: https://github.com/Alexays/Waybar
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
[awesome-niri]: https://github.com/Vortriz/awesome-niri
|
||||
[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
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[Paneru]: https://github.com/karinushka/paneru
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
|
||||
[OpenTabletDriver]: https://opentabletdriver.net/
|
||||
[DankMaterialShell]: https://danklinux.com/
|
||||
[Noctalia]: https://noctalia.dev/
|
||||
|
||||
@@ -40,5 +40,5 @@ def _badge_for_version(preposition: str, version: str):
|
||||
# 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/YaLTeR/niri/releases/tag/v{version}"
|
||||
path = f"https://github.com/niri-wm/niri/releases/tag/v{version}"
|
||||
return f"<span class=\"badge\">[{preposition}: {version}]({path})</span>"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
site_name: niri
|
||||
docs_dir: wiki
|
||||
site_url: https://yalter.github.io/niri
|
||||
repo_url: https://github.com/YaLTeR/niri
|
||||
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
|
||||
|
||||
@@ -85,8 +85,10 @@ nav:
|
||||
- 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
|
||||
- Security Model: Security-Model.md
|
||||
- Accessibility: Accessibility.md
|
||||
- Name and Logo: Name-and-Logo.md
|
||||
- FAQ: FAQ.md
|
||||
@@ -110,6 +112,7 @@ nav:
|
||||
- Design Principles: Development:-Design-Principles.md
|
||||
- Developing niri: Development:-Developing-niri.md
|
||||
- Documenting niri: Development:-Documenting-niri.md
|
||||
- Releasing niri: Development:-Releasing-niri.md
|
||||
- Fractional Layout: Development:-Fractional-Layout.md
|
||||
- Redraw Loop: Development:-Redraw-Loop.md
|
||||
- Animation Timing: Development:-Animation-Timing.md
|
||||
|
||||
@@ -33,6 +33,8 @@ JetBrains IDEs can run directly on Wayland, but it's not the default.
|
||||
|
||||
For JetBrainsRuntime > 17, you can set the flag `-Dawt.toolkit.name=WLToolkit` inside of `help -> edit custom vm options -> add`.
|
||||
|
||||
If the settings window fails to load under Wayland, and the UI becomes unresponsive afterwards, also set the flag `-Dsun.awt.wl.WindowDecorationStyle=builtin` in the custom vm options. This gives the settings window a titlebar, but it at least makes the IDE functional.
|
||||
|
||||
### WezTerm
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -107,6 +107,8 @@ debug {
|
||||
|
||||
### `force-disable-connectors-on-resume`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Force-disables all outputs upon resuming niri (TTY switch or waking up from suspend).
|
||||
This causes a modeset/screen blank on all outputs.
|
||||
|
||||
@@ -322,27 +324,6 @@ debug {
|
||||
}
|
||||
```
|
||||
|
||||
### `keep-max-bpc-unchanged`
|
||||
|
||||
<sup>Since: 25.08</sup>
|
||||
|
||||
When connecting monitors, niri sets their max bpc to 8 in order to reduce display bandwidth and to potentially allow more monitors to be connected at once.
|
||||
Restricting bpc to 8 is not a problem since we don't support HDR or color management yet and can't really make use of higher bpc.
|
||||
|
||||
Apparently, setting max bpc to 8 breaks some displays driven by AMDGPU.
|
||||
If this happens to you, set this debug flag, which will prevent niri from changing max bpc.
|
||||
AMDGPU bug report: https://gitlab.freedesktop.org/drm/amd/-/issues/4487.
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
This setting is deprecated and does nothing: niri no longer sets max bpc.
|
||||
The old niri behavior with this setting enabled matches the new behavior.
|
||||
|
||||
```kdl
|
||||
debug {
|
||||
keep-max-bpc-unchanged
|
||||
}
|
||||
```
|
||||
|
||||
### Key Bindings
|
||||
|
||||
These are not debug options, but rather key bindings.
|
||||
|
||||
@@ -16,6 +16,12 @@ Settings from included files will be merged with the settings from the main conf
|
||||
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
|
||||
@@ -116,7 +122,7 @@ window-rule {
|
||||
|
||||
### Optional includes
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
By default, including a nonexistent file will cause an error.
|
||||
You can allow nonexistent includes by setting `optional=true`:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
In this section you can configure input devices like keyboard and mouse, and some input-related options.
|
||||
|
||||
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `tablet`, `touch`.
|
||||
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `trackball`, `tablet`, `touch`.
|
||||
Settings in those sections will apply to every device of that type.
|
||||
Currently, there's no way to configure specific devices individually (but that is planned).
|
||||
|
||||
@@ -89,6 +89,8 @@ input {
|
||||
tablet {
|
||||
// off
|
||||
map-to-output "eDP-1"
|
||||
// map-to-focused-output
|
||||
// map-to-focused-window
|
||||
// left-handed
|
||||
// calibration-matrix 1.0 0.0 0.0 0.0 1.0 0.0
|
||||
}
|
||||
@@ -281,6 +283,16 @@ Valid output names are the same as the ones used for output configuration.
|
||||
|
||||
<sup>Since: 0.1.7</sup> When a tablet is not mapped to any output, it will map to the union of all connected outputs, without aspect ratio correction.
|
||||
|
||||
Settings specific to `tablet`:
|
||||
|
||||
- `map-to-focused-output`: <sup>Since: 26.04</sup> will map the tablet to the focused output, takes precedence over `map-to-output`.
|
||||
|
||||
- `map-to-focused-window`: <sup>Since: next release</sup> will map the tablet to the focused window's geometry, takes precedence over `map-to-focused-output` and `map-to-output`.
|
||||
Falls back to those when no window is focused (for example, in the overview).
|
||||
|
||||
When the tablet is also mapped to a specific output via `map-to-output`, the `map-to-focused-window` flag will map the tablet to the active window on that output.
|
||||
If the tablet isn't mapped to any specific output, it will map the tablet to the current focused window regardless of where it is.
|
||||
|
||||
### General Settings
|
||||
|
||||
These settings are not specific to a particular input device.
|
||||
|
||||
@@ -19,7 +19,7 @@ You can find documentation for various sections of the config on these wiki page
|
||||
### Loading
|
||||
|
||||
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/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
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.
|
||||
Please use the default configuration file as the starting point for your custom configuration.
|
||||
|
||||
The configuration is live-reloaded.
|
||||
|
||||
@@ -382,7 +382,7 @@ binds {
|
||||
}
|
||||
```
|
||||
|
||||
<sup>Since: next release</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
<sup>Since: 26.04</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
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).
|
||||
|
||||
```kdl
|
||||
|
||||
@@ -14,6 +14,7 @@ Here are all matchers and properties that a layer rule could have:
|
||||
layer-rule {
|
||||
match namespace="waybar"
|
||||
match at-startup=true
|
||||
match layer="top"
|
||||
|
||||
// Properties that apply continuously.
|
||||
opacity 0.5
|
||||
@@ -34,6 +35,25 @@ layer-rule {
|
||||
geometry-corner-radius 12
|
||||
place-within-backdrop true
|
||||
baba-is-float true
|
||||
|
||||
background-effect {
|
||||
xray true
|
||||
blur true
|
||||
noise 0.05
|
||||
saturation 3
|
||||
}
|
||||
|
||||
popups {
|
||||
opacity 0.5
|
||||
geometry-corner-radius 6
|
||||
|
||||
background-effect {
|
||||
xray true
|
||||
blur true
|
||||
noise 0.05
|
||||
saturation 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -69,6 +89,22 @@ layer-rule {
|
||||
}
|
||||
```
|
||||
|
||||
#### `layer`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Matches surfaces on this layer-shell layer.
|
||||
Can be `"background"`, `"bottom"`, `"top"`, or `"overlay"`.
|
||||
|
||||
```kdl
|
||||
// Make all overlay-layer surfaces FLOAT.
|
||||
layer-rule {
|
||||
match layer="overlay"
|
||||
|
||||
baba-is-float true
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Properties
|
||||
|
||||
These properties apply continuously to open layer-shell surfaces.
|
||||
@@ -191,3 +227,68 @@ layer-rule {
|
||||
baba-is-float true
|
||||
}
|
||||
```
|
||||
|
||||
#### `background-effect`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override the background effect options for this surface.
|
||||
|
||||
- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
|
||||
- `blur`: set to `true` to enable blur behind this surface, or `false` to force-disable it.
|
||||
- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
|
||||
- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
|
||||
|
||||
See the [window effects page](./Window-Effects.md) for an overview of background effects.
|
||||
|
||||
```kdl
|
||||
// Make top and overlay layers use the regular blur (if enabled),
|
||||
// while bottom and background layers keep using the efficient xray blur.
|
||||
layer-rule {
|
||||
match layer="top"
|
||||
match layer="overlay"
|
||||
|
||||
background-effect {
|
||||
xray false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `popups`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override properties for this layer surface's pop-ups (e.g. a menu opened by clicking an item in Waybar).
|
||||
|
||||
The properties work the same way as the corresponding layer-rule properties, except that they apply to the layer surface's pop-ups rather than to the layer surface itself.
|
||||
|
||||
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
|
||||
Other properties apply independently.
|
||||
|
||||
> [!NOTE]
|
||||
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
|
||||
>
|
||||
> Some desktop shells will emulate pop-ups by drawing something that looks like a pop-up inside a regular layer surface.
|
||||
> As far as niri is concerned, those are just layer surfaces and not pop-ups, so this block won't apply to them.
|
||||
>
|
||||
> This block also does not affect input-method pop-ups, such as Fcitx.
|
||||
|
||||
```kdl
|
||||
// Blur the background behind Waybar popup menus.
|
||||
layer-rule {
|
||||
match namespace="^waybar$"
|
||||
|
||||
popups {
|
||||
// Match the default GTK 3 popup corner radius.
|
||||
geometry-corner-radius 6
|
||||
opacity 0.85
|
||||
|
||||
background-effect {
|
||||
blur true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the layer surface correctly sets its Wayland geometry to exclude any shadows.
|
||||
Pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
|
||||
|
||||
@@ -177,6 +177,7 @@ layout {
|
||||
### `preset-column-widths`
|
||||
|
||||
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (Mod+Shift+R) to toggle in reverse.
|
||||
|
||||
`proportion` sets the width as a fraction of the output width, taking gaps into account.
|
||||
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
|
||||
@@ -228,7 +229,8 @@ layout {
|
||||
|
||||
<sup>Since: 0.1.9</sup>
|
||||
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between.
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Ctrl+Shift+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-window-height-back` action (not bound by default) to toggle in reverse.
|
||||
|
||||
`proportion` sets the height as a fraction of the output height, taking gaps into account.
|
||||
The default preset heights are <sup>1</sup>⁄<sub>3</sub>, <sup>1</sup>⁄<sub>2</sub> and <sup>2</sup>⁄<sub>3</sub> of the output.
|
||||
|
||||
@@ -54,6 +54,14 @@ hotkey-overlay {
|
||||
config-notification {
|
||||
disable-failed
|
||||
}
|
||||
|
||||
blur {
|
||||
// off
|
||||
passes 3
|
||||
offset 3.0
|
||||
noise 0.02
|
||||
saturation 1.5
|
||||
}
|
||||
```
|
||||
|
||||
### `spawn-at-startup`
|
||||
@@ -320,3 +328,82 @@ 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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -15,6 +15,7 @@ output "eDP-1" {
|
||||
variable-refresh-rate // on-demand=true
|
||||
focus-at-startup
|
||||
backdrop-color "#001100"
|
||||
// max-bpc 8
|
||||
|
||||
hot-corners {
|
||||
// off
|
||||
@@ -279,6 +280,27 @@ output "HDMI-A-1" {
|
||||
}
|
||||
```
|
||||
|
||||
### `max-bpc`
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
|
||||
Set the maximum bits per channel (BPC) for this output.
|
||||
|
||||
You *do not* need to set this option normally.
|
||||
It influences the encoding of the display signal on the wire and *is not* directly related to the color bitness or framebuffer format.
|
||||
|
||||
Setting `max-bpc` to a low value may help if you hit a bandwidth issue (can't set a monitor configuration that works on other compositor).
|
||||
Otherwise, you're advised to leave it unset (keeping a default, usually high value) and let the GPU driver figure things out automatically.
|
||||
|
||||
Valid values are `6`, `8`, `10`, `12`, `14`, `16`.
|
||||
|
||||
```kdl
|
||||
// Set 8 max-bpc on HDMI-A-1 to lower the bandwidth.
|
||||
output "HDMI-A-1" {
|
||||
max-bpc 8
|
||||
}
|
||||
```
|
||||
|
||||
### `hot-corners`
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
|
||||
@@ -100,6 +100,25 @@ window-rule {
|
||||
tiled-state true
|
||||
baba-is-float true
|
||||
|
||||
background-effect {
|
||||
xray true
|
||||
blur true
|
||||
noise 0.05
|
||||
saturation 3
|
||||
}
|
||||
|
||||
popups {
|
||||
opacity 0.5
|
||||
geometry-corner-radius 15
|
||||
|
||||
background-effect {
|
||||
xray true
|
||||
blur true
|
||||
noise 0.05
|
||||
saturation 3
|
||||
}
|
||||
}
|
||||
|
||||
min-width 100
|
||||
max-width 200
|
||||
min-height 300
|
||||
@@ -909,6 +928,95 @@ https://github.com/user-attachments/assets/3f4cb1a4-40b2-4766-98b7-eec014c19509
|
||||
|
||||
</video>
|
||||
|
||||
#### `background-effect`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override the background effect options for this window.
|
||||
|
||||
- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
|
||||
- `blur`: set to `true` to enable blur behind this window, or `false` to force-disable it.
|
||||
- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
|
||||
- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
|
||||
|
||||
See the [window effects page](./Window-Effects.md) for an overview of background effects.
|
||||
|
||||
```kdl
|
||||
// Make floating windows use the regular blur (if enabled),
|
||||
// while tiled windows keep using the efficient xray blur.
|
||||
//
|
||||
// Warning: non-xray blur is currently experimental and has known limitations.
|
||||
// In particular, it doesn't work during window opening and closing animations.
|
||||
window-rule {
|
||||
match is-floating=true
|
||||
|
||||
background-effect {
|
||||
xray false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `popups`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override properties for this window's pop-ups (menus and tooltips).
|
||||
|
||||
The properties work the same way as the corresponding window-rule properties, except that they apply to the window's pop-ups rather than to the window itself.
|
||||
|
||||
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
|
||||
Other properties apply independently.
|
||||
|
||||
> [!NOTE]
|
||||
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
|
||||
>
|
||||
> Examples of things that look like pop-ups that won't work:
|
||||
>
|
||||
> - Fully emulated by the client, i.e. not a pop-up at all, the client just draws something that looks like a pop-up inside its window.
|
||||
> These are common in game engines and in web apps, e.g. the right click menu in Google Docs or in Electron apps like Discord.
|
||||
>
|
||||
> - Uses a wl-subsurface instead of an xdg-popup.
|
||||
> Common in older apps using GTK 3, notably Firefox still uses these for some menus.
|
||||
> Subsurfaces are an indivisible part of a surface and they aren't usually pop-ups, so it wouldn't make sense for niri to apply these rules to them.
|
||||
>
|
||||
> These emulated pop-ups come with other downsides: they cannot reliably extend outside their window, and if the app tries to do that, they will be clipped by rules such as `clip-to-geometry`.
|
||||
> So most modern apps will correctly use xdg-popup, which is the intended way to show pop-ups on Wayland.
|
||||
>
|
||||
> This block also does not affect input-method pop-ups, such as Fcitx.
|
||||
>
|
||||
> For pop-ups created by your desktop shell or desktop components, use the corresponding [layer rule](./Configuration:-Layer-Rules.md#popups).
|
||||
|
||||
```kdl
|
||||
// Blur the background behind pop-up menus in Nautilus.
|
||||
window-rule {
|
||||
match app-id="Nautilus"
|
||||
|
||||
popups {
|
||||
// Matches the default libadwaita pop-up corner radius.
|
||||
geometry-corner-radius 15
|
||||
|
||||
// Note: it'll look better to set background opacity
|
||||
// through your GTK theme CSS and not here.
|
||||
// This is just an example that makes it look obvious.
|
||||
opacity 0.5
|
||||
|
||||
background-effect {
|
||||
blur true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the window correctly sets its Wayland geometry to exclude any shadows.
|
||||
For example, GTK 4 pop-ups with pointing arrows (`has-arrow=true` property) are *not* rounded rectangles—the arrow sticks out—so if you enable blur, it will also stick out of the pop-up.
|
||||
|
||||
| Correct | Wrong |
|
||||
|-----------------------------------------------------|--------------------------------------------------------------------------------|
|
||||
| The pop-up is a rounded rectangle. Blur looks fine. | The pop-up is not a rounded rectangle. Blur extends above, where the arrow is. |
|
||||
|  |  |
|
||||
|
||||
These pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
|
||||
|
||||
#### Size Overrides
|
||||
|
||||
You can amend the window's minimum and maximum size in logical pixels.
|
||||
|
||||
@@ -74,7 +74,7 @@ Here are some design considerations for the window layout logic.
|
||||
|
||||
## Default config
|
||||
|
||||
The [default config](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
|
||||
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.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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/YaLTeR/niri/wiki)
|
||||
- [The documentation site](https://yalter.github.io/niri/)
|
||||
- [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
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
This is a checklist of things to release a new niri version.
|
||||
|
||||
We'll use `26.04` as the example new version.
|
||||
When making a patch release, append the patch number like `26.04.1`.
|
||||
|
||||
## Prepare the release notes
|
||||
|
||||
Plan for a few days of work, this usually takes a while.
|
||||
|
||||
During this process, also check:
|
||||
|
||||
- that all additions are marked with "next release" on the wiki,
|
||||
- if anything needs updating in `README.md`.
|
||||
|
||||
## Bump version
|
||||
|
||||
We use `year.month.patch` versioning.
|
||||
If the month contains a leading zero, drop it from the crate version (Cargo requirement).
|
||||
|
||||
You can use the command from [cargo-edit](https://github.com/killercup/cargo-edit):
|
||||
|
||||
```
|
||||
cargo set-version 26.4.0
|
||||
```
|
||||
|
||||
Then, manually update version in:
|
||||
|
||||
- `[package.metadata.generate-rpm]` in Cargo.toml
|
||||
- Dependency example in `niri-ipc/README.md`
|
||||
- Dependency example in `niri-ipc/src/lib.rs`
|
||||
|
||||
Do a full text search for the old version to make sure there are no other places.
|
||||
|
||||
## Replace all "Since: next release" mentions
|
||||
|
||||
Do a full text search for `next release`, replace everything with the new version number.
|
||||
|
||||
## Build, test, push, and have the CI run
|
||||
|
||||
Run all tests:
|
||||
|
||||
```
|
||||
RUN_SLOW_TESTS=1 cargo test --release --all
|
||||
```
|
||||
|
||||
- Run `cargo package -p niri-ipc` and make sure it succeeds.
|
||||
- Make sure the CI passes.
|
||||
- Make sure the niri-git COPR build passes.
|
||||
|
||||
## Trigger the "Prepare release" workflow on GitHub Actions
|
||||
|
||||
Set the "Public version" input to a version like `26.04`.
|
||||
|
||||
This workflow will:
|
||||
|
||||
- do some pre-release checks like grepping the wiki for "next version",
|
||||
- make a vendored dependency archive,
|
||||
- build and test niri with that dependency archive,
|
||||
- draft a new GitHub release with the archive attached.
|
||||
It will NOT override an existing draft release with the same name so the release notes are safe.
|
||||
|
||||
Make sure it succeeds and grab the vendored dependency archive that it produces.
|
||||
|
||||
## Update the niri COPR spec, update licenses in .spec.rpkg
|
||||
|
||||
You can grab the previous spec from [the last build](https://copr.fedorainfracloud.org/coprs/yalter/niri/builds/) in the COPR.
|
||||
|
||||
- Update version global to `26.04`.
|
||||
- Update commit global to the commit hash corresponding to the release commit.
|
||||
You can use `git rev-parse HEAD`.
|
||||
- Reset the `Release:` number to 1 if it was higher.
|
||||
|
||||
To run a test build, you can download the vendored dependency archive from the last step.
|
||||
Comment/uncomment `Source:` and `%autosetup` lines accordingly.
|
||||
|
||||
Download the source files:
|
||||
|
||||
```
|
||||
spectool -g niri.spec
|
||||
```
|
||||
|
||||
Build RPMs:
|
||||
|
||||
```
|
||||
fedpkg --release 44 mockbuild
|
||||
```
|
||||
|
||||
During the build, it will print the list of licenses.
|
||||
Update it in both the COPR spec and in `niri.spec.rpkg` accordingly.
|
||||
|
||||
If you had to update `niri.spec.rpkg` and therefore make another commit to the niri repo, make sure to update the commit hash in the COPR spec again.
|
||||
|
||||
Revert any temporary changes that you did to the COPR spec for local testing.
|
||||
|
||||
## Create and push the release git tag
|
||||
|
||||
The tag starts with a `v`:
|
||||
|
||||
```
|
||||
git tag -am "v26.04 release" v26.04
|
||||
git push origin v26.04
|
||||
```
|
||||
|
||||
While you can let GitHub create the tag automatically upon creating the release, this is not recommended.
|
||||
GitHub creates a *lightweight* tag, but we want an annotated tag that plays better with various tooling.
|
||||
|
||||
## Publish the release on GitHub
|
||||
|
||||
- Either upload the vendored dependencies file to your draft release with the release notes, or move the release notes to the GitHub-created release (the difference is that it's attributed to github-actions).
|
||||
- Set the tag to `v26.04`.
|
||||
- Set the release title to `v26.04`.
|
||||
- Check "Create a discussion for this release".
|
||||
|
||||
## Publish the niri-ipc crate
|
||||
|
||||
```
|
||||
cargo publish -p niri-ipc
|
||||
```
|
||||
|
||||
## Kick off the COPR build
|
||||
|
||||
Upload on the web or:
|
||||
|
||||
```
|
||||
copr-cli build niri niri.spec
|
||||
```
|
||||
|
||||
## Announce the release
|
||||
|
||||
Chat rooms, social media, etc.
|
||||
|
||||
## Update wayland.app protocol data
|
||||
|
||||
- Install [wlprobe](https://github.com/PolyMeilex/wlprobe).
|
||||
- Clone https://github.com/vially/wayland-explorer.
|
||||
- Generate data:
|
||||
|
||||
```
|
||||
wlprobe > ./src/data/compositors/niri.json
|
||||
```
|
||||
|
||||
- Manually add `"version": "26.04"`, then clean up the diff from unrelated changes, for example:
|
||||
- The number of `wl_output`s will change depending on how many monitors you have connected.
|
||||
- The number of `wp_drm_lease_device_v1` will change depending on your number of GPUs.
|
||||
- `org_kde_kwin_server_decoration_manager` and `zxdg_decoration_manager_v1` will only appear with `prefer-no-csd`.
|
||||
- Create a pull request.
|
||||
@@ -40,6 +40,20 @@ hotkey-overlay {
|
||||
}
|
||||
```
|
||||
|
||||
### 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).
|
||||
@@ -66,14 +80,12 @@ I wouldn't be too surprised if, down the road, xwayland-satellite becomes the st
|
||||
|
||||
### Can I enable blur behind semitransparent windows?
|
||||
|
||||
Not yet, follow/upvote [this issue](https://github.com/YaLTeR/niri/issues/54).
|
||||
|
||||
There's also [a PR](https://github.com/YaLTeR/niri/pull/1634) adding blur to niri which you can build and run manually.
|
||||
Keep in mind that it's an experimental implementation that may have problems and performance concerns.
|
||||
<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/YaLTeR/niri/issues/932).
|
||||
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`).
|
||||
@@ -82,7 +94,7 @@ For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature
|
||||
|
||||
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/YaLTeR/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
|
||||
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?
|
||||
|
||||
@@ -92,7 +104,7 @@ Listen to the event stream for a new window opening, then call an action like `c
|
||||
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/YaLTeR/niri/discussions/1125.
|
||||
- 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?
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ You can make a window open in a maximized column with the [`open-maximized true`
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
|
||||
You can maximize an individual window via `maximize-window-to-edges`.
|
||||
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.
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ sudo dnf install niri dms
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
Arch Linux (via [paru](https://github.com/morganamilo/paru)):
|
||||
Arch Linux:
|
||||
```
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty
|
||||
paru -S dms-shell-bin matugen cava qt6-multimedia-ffmpeg
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty dms-shell-niri matugen cava qt6-multimedia-ffmpeg
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
@@ -147,13 +146,10 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
|
||||
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
|
||||
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the bottom window in the focused column into its own column |
|
||||
| <kbd>Mod</kbd><kbd>[</kbd> | Consume or expel the focused window to the left |
|
||||
| <kbd>Mod</kbd><kbd>]</kbd> | Consume or expel the focused window to the right |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column heights |
|
||||
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> and <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column widths forward and back |
|
||||
| <kbd>Mod</kbd><kbd>M</kbd> | Maximize window |
|
||||
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
|
||||
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
|
||||
|
||||
@@ -26,7 +26,7 @@ To get a taste of the events, run `niri msg event-stream`.
|
||||
Though, this is more of a debug function than anything.
|
||||
You can get raw events from `niri msg --json event-stream`, or by connecting to the niri socket and requesting an event stream manually.
|
||||
|
||||
You can find the full list of events along with documentation [here](https://yalter.github.io/niri/niri_ipc/enum.Event.html).
|
||||
You can find the full list of events along with documentation [here](https://niri-wm.github.io/niri/niri_ipc/enum.Event.html).
|
||||
|
||||
### Programmatic Access
|
||||
|
||||
@@ -57,7 +57,7 @@ $ env NIRI_SOCKET=./temp.sock niri msg action focus-workspace 2
|
||||
{"Action":{"FocusWorkspace":{"reference":{"Index":2}}}}
|
||||
```
|
||||
|
||||
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://yalter.github.io/niri/niri_ipc/).
|
||||
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://niri-wm.github.io/niri/niri_ipc/).
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@ First, for creating a niri package, see the [Packaging](./Packaging-niri.md) pag
|
||||
### 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/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
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.
|
||||
|
||||
You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
|
||||
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
|
||||
|
||||
@@ -32,7 +36,7 @@ Make sure your system installer sets the keyboard layout via systemd-localed, an
|
||||
### Autostart
|
||||
|
||||
Niri works with the normal systemd autostart.
|
||||
The default [niri.service](https://github.com/YaLTeR/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
|
||||
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.
|
||||
@@ -59,3 +63,7 @@ Alternatively, some desktop environments and shells work with niri, and can give
|
||||
- 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).
|
||||
|
||||
### Security model
|
||||
|
||||
See the [Security Model](./Security-Model.md) page for an overview of niri's security model.
|
||||
|
||||
@@ -47,7 +47,7 @@ Set them as automatically-installed optional dependencies, if possible.
|
||||
Working hardware acceleration is required for running niri.
|
||||
- Some notification daemon like `mako`, generally required for apps to work correctly.
|
||||
|
||||
Finally, you may want to auto-install some of the applications bound in niri's [default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
|
||||
Finally, you may want to auto-install some of the applications bound in niri's [default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
|
||||
|
||||
### Running tests
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ 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/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
|
||||
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://yalter.github.io/niri/
|
||||
If you're not already here, check out our new wiki website! https://niri-wm.github.io/niri/
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -53,12 +53,12 @@ It shows up as "niri Dynamic Cast Target" in the screencast window dialog.
|
||||
|
||||

|
||||
|
||||
When you select it, it will start as an empty, transparent video stream.
|
||||
Then, you can use the following binds to change what it shows:
|
||||
Choose it, then use the following binds to change what it shows.
|
||||
The stream won't start until you make your first target selection.
|
||||
|
||||
- `set-dynamic-cast-window` to cast the focused window.
|
||||
- `set-dynamic-cast-monitor` to cast the focused monitor.
|
||||
- `clear-dynamic-cast-target` to go back to an empty stream.
|
||||
- `clear-dynamic-cast-target` to reset to an empty video stream.
|
||||
|
||||
You can also use these actions from the command line, for example to interactively pick which window to cast:
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
Niri assumes that programs running unsandboxed on the host are **trusted**.
|
||||
|
||||
This is a reasonable assumption because programs running on the host have a wide variety of ways to get all access they need, even without niri.
|
||||
For instance:
|
||||
|
||||
- They can set `$LD_PRELOAD` in `.bashrc` or similar files to load an arbitrary library into all processes.
|
||||
- They can replace binaries in `$PATH` with malicious code.
|
||||
- They can interpose any socket in `$XDG_RUNTIME_DIR`, like Wayland, and do keylogging or record window contents.
|
||||
- They can scan the filesystem for secrets: SSH keys, password stores, etc.
|
||||
- They can connect to an unlocked keyring and steal credentials.
|
||||
- And so on and so forth.
|
||||
|
||||
## Unsandboxed clients
|
||||
|
||||
Anything with access to niri's Wayland socket can, among other things:
|
||||
|
||||
- Record the user's screen via [wlr-screencopy](https://wayland.app/protocols/wlr-screencopy-unstable-v1).
|
||||
- Emulate input via [wlr-virtual-pointer](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) and [virtual-keyboard](https://wayland.app/protocols/virtual-keyboard-unstable-v1).
|
||||
- Get the user's clipboard contents via [wlr-data-control](https://wayland.app/protocols/ext-data-control-v1).
|
||||
- Create arbitrary fullscreen surfaces through [wlr-layer-shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) that can steal the user's input, pretend to be a password entry, or lock the user out of their session.
|
||||
- Kill a running lockscreen, create a new lock surface, and tell niri to unlock a locked session.
|
||||
|
||||
Anything with access to niri's [IPC](./IPC.md) socket can, among other things:
|
||||
|
||||
- Spawn a Wayland client which can do everything in the list above.
|
||||
|
||||
Anything with access to niri's D-Bus interfaces can, among other things:
|
||||
|
||||
- Record the user's screen via the screencast interface.
|
||||
- Fully listen to and emulate input from the user's keyboard via the accessibility interface.
|
||||
|
||||
Also, while niri doesn't directly integrate Xwayland, it's worth reminding that anything with access to the X11 `$DISPLAY` (which comes both as a socket file on disk **and** as an abstract socket in the network namespace) can intercept and emulate all input and record the contents of any X11 windows on the same `$DISPLAY` (but not Wayland windows).
|
||||
|
||||
## Running untrusted clients
|
||||
|
||||
Considering all of the above, for running untrusted clients, you need a proper sandbox that:
|
||||
|
||||
- Removes niri's IPC socket.
|
||||
- Prevents D-Bus access to host services.
|
||||
- Uses a filtered Wayland socket.
|
||||
|
||||
For creating a filtered Wayland socket, you can use the [security-context](https://wayland.app/protocols/security-context-v1) protocol which niri implements.
|
||||
All unsafe protocols are made inaccessible through this filtered Wayland socket.
|
||||
|
||||
One sandbox that satisfies all of these criteria is the [Flatpak](https://flatpak.org/) sandbox.
|
||||
|
||||
Importantly, filtering just the Wayland socket (and leaving, for example, unrestricted D-Bus access) is **not enough** to prevent untrusted clients from doing bad things.
|
||||
|
||||
## Lock screen
|
||||
|
||||
When the session is locked via [ext-session-lock](https://wayland.app/protocols/ext-session-lock-v1), most actions (keybindings) are automatically disabled.
|
||||
Only a very small set of safe actions is allowed.
|
||||
In particular, spawning will not work, with the exception of binds explicitly configured with `allow-when-locked=true`.
|
||||
|
||||
Importantly, the **quit** action is allowed—you can always quit niri, even when on a lock screen.
|
||||
Therefore, you must ensure that quitting niri does not drop you into an unprotected TTY commandline.
|
||||
Usually, a display manager, like GDM, will do this for you: when niri exits (via the quit bind or if it crashes), it'll put you back into a safe password prompt.
|
||||
|
||||
Other than quitting, the only way to exit a lock screen is for the lock screen client to tell niri to unlock the session.
|
||||
If the lock screen client crashes, the session remains locked with a solid red background.
|
||||
In this case, another lock screen client can take over (so you can start a fresh lock screen if it crashes, and still unlock your session).
|
||||
@@ -0,0 +1,87 @@
|
||||
### 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.
|
||||
@@ -47,7 +47,7 @@ It will open as a new window.
|
||||
|
||||
This method involves invoking XWayland directly and running it as its own window, it also requires an extra X11 window manager running inside it.
|
||||
|
||||

|
||||

|
||||
|
||||
Here's how you do it:
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
* [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)
|
||||
* [Security Model](./Security-Model.md)
|
||||
* [Accessibility](./Accessibility.md)
|
||||
* [Name and Logo](./Name-and-Logo.md)
|
||||
* [FAQ](./FAQ.md)
|
||||
@@ -41,6 +43,7 @@
|
||||
* [Design Principles](./Development:-Design-Principles.md)
|
||||
* [Developing niri](./Development:-Developing-niri.md)
|
||||
* [Documenting niri](./Development:-Documenting-niri.md)
|
||||
* [Releasing niri](./Development:-Releasing-niri.md)
|
||||
* [Fractional Layout](./Development:-Fractional-Layout.md)
|
||||
* [Redraw Loop](./Development:-Redraw-Loop.md)
|
||||
* [Animation Timing](./Development:-Animation-Timing.md)
|
||||
|
||||
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 9.3 KiB |
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1757967192,
|
||||
"narHash": "sha256-/aA9A/OBmnuOMgwfzdsXRusqzUpd8rQnQY8jtrHK+To=",
|
||||
"lastModified": 1781607440,
|
||||
"narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0d7c15863b251a7a50265e57c1dca1a7add2e291",
|
||||
"rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,28 +18,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757989933,
|
||||
"narHash": "sha256-9cpKYWWPCFhgwQTww8S94rTXgg8Q8ydFv9fXM6I8xQM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "8249aa3442fb9b45e615a35f39eca2fe5510d7c3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,24 +2,15 @@
|
||||
{
|
||||
description = "Niri: A scrollable-tiling Wayland compositor.";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
# NOTE: This is not necessary for end users
|
||||
# You can omit it with `inputs.rust-overlay.follows = ""`
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
revision = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
niri-package =
|
||||
{
|
||||
lib,
|
||||
@@ -46,7 +37,7 @@
|
||||
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "niri";
|
||||
version = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
version = revision;
|
||||
|
||||
src = lib.fileset.toSource {
|
||||
root = ./.;
|
||||
@@ -107,7 +98,7 @@
|
||||
buildNoDefaultFeatures = true;
|
||||
|
||||
# ever since this commit:
|
||||
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
|
||||
# https://github.com/niri-wm/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.
|
||||
@@ -134,12 +125,12 @@
|
||||
''
|
||||
+ lib.optionalString withSystemd ''
|
||||
install -Dm755 resources/niri-session $out/bin/niri-session
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/lib/systemd/user
|
||||
'';
|
||||
|
||||
env = {
|
||||
# Force linking with libEGL and libwayland-client
|
||||
# so they can be discovered by `dlopen()`
|
||||
# Force linking with libEGL and libwayland-client so they end up in RPATH and
|
||||
# can be discovered by `dlopen()`
|
||||
RUSTFLAGS = toString (
|
||||
map (arg: "-C link-arg=" + arg) [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
@@ -148,7 +139,7 @@
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
NIRI_BUILD_COMMIT = self.shortRev;
|
||||
NIRI_BUILD_COMMIT = revision;
|
||||
};
|
||||
|
||||
passthru = {
|
||||
@@ -157,7 +148,7 @@
|
||||
|
||||
meta = {
|
||||
description = "Scrollable-tiling Wayland compositor";
|
||||
homepage = "https://github.com/YaLTeR/niri";
|
||||
homepage = "https://github.com/niri-wm/niri";
|
||||
license = lib.licenses.gpl3Only;
|
||||
mainProgram = "niri";
|
||||
platforms = lib.platforms.linux;
|
||||
@@ -181,33 +172,20 @@
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system};
|
||||
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
|
||||
rustfmt' = pkgs.rustfmt.override { asNightly = true; };
|
||||
inherit (self.packages.${system}) niri;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
# We don't use the toolchain from nixpkgs
|
||||
# because we prefer a nightly toolchain
|
||||
# and we *require* a nightly rustfmt
|
||||
(rust-bin.selectLatestNightlyWith (
|
||||
toolchain:
|
||||
toolchain.default.override {
|
||||
extensions = [
|
||||
# includes already:
|
||||
# rustc
|
||||
# cargo
|
||||
# rust-std
|
||||
# rust-docs
|
||||
# rustfmt-preview
|
||||
# clippy-preview
|
||||
"rust-analyzer"
|
||||
"rust-src"
|
||||
];
|
||||
}
|
||||
))
|
||||
pkgs.cargo-insta
|
||||
];
|
||||
packages = builtins.attrValues {
|
||||
inherit (pkgs)
|
||||
rustc
|
||||
cargo
|
||||
clippy
|
||||
cargo-insta
|
||||
;
|
||||
inherit rustfmt';
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
@@ -224,8 +202,8 @@
|
||||
# It is required for `dlopen()` to work on some libraries; see the comment
|
||||
# in the package expression
|
||||
#
|
||||
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
|
||||
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
|
||||
# This should only be set with `RUSTFLAGS="$RUSTFLAGS -C your-flags"`
|
||||
RUSTFLAGS = niri.RUSTFLAGS;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.8.1"
|
||||
csscolorparser = "0.8.3"
|
||||
knuffel = "3.2.0"
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.11.0", path = "../niri-ipc" }
|
||||
regex = "1.12.2"
|
||||
niri-ipc = { version = "26.4.0", path = "../niri-ipc" }
|
||||
regex = "1.12.3"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
@@ -1006,6 +1006,103 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Blur {
|
||||
pub off: bool,
|
||||
pub passes: u8,
|
||||
pub offset: f64,
|
||||
pub noise: f64,
|
||||
pub saturation: f64,
|
||||
}
|
||||
|
||||
impl Default for Blur {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
passes: 3,
|
||||
offset: 3.,
|
||||
noise: 0.02,
|
||||
saturation: 1.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct BlurPart {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child)]
|
||||
pub on: bool,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub passes: Option<u8>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub offset: Option<FloatOrInt<0, 100>>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub noise: Option<FloatOrInt<0, 1000>>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub saturation: Option<FloatOrInt<0, 1000>>,
|
||||
}
|
||||
|
||||
impl MergeWith<BlurPart> for Blur {
|
||||
fn merge_with(&mut self, part: &BlurPart) {
|
||||
self.off |= part.off;
|
||||
if part.on {
|
||||
self.off = false;
|
||||
}
|
||||
|
||||
merge_clone!((self, part), passes);
|
||||
merge!((self, part), offset, noise, saturation);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct BackgroundEffectRule {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub xray: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub blur: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub noise: Option<FloatOrInt<0, 1000>>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub saturation: Option<FloatOrInt<0, 1000>>,
|
||||
}
|
||||
|
||||
/// Resolved background effect rule.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct BackgroundEffect {
|
||||
/// Whether to render with xray effect (see through).
|
||||
///
|
||||
/// - `None`: xray if any background effect is active
|
||||
/// - `Some(false)`: no xray
|
||||
/// - `Some(true)`: xray even if no other background effect is active
|
||||
pub xray: Option<bool>,
|
||||
|
||||
/// Whether to blur the background.
|
||||
///
|
||||
/// - `None`: blur when the window/layer requests it (e.g. through ext-background-effect
|
||||
/// protocol)
|
||||
/// - `Some(false)`: never blur
|
||||
/// - `Some(true)`: always blur
|
||||
pub blur: Option<bool>,
|
||||
|
||||
pub noise: Option<f64>,
|
||||
pub saturation: Option<f64>,
|
||||
}
|
||||
|
||||
impl MergeWith<BackgroundEffectRule> for BackgroundEffect {
|
||||
fn merge_with(&mut self, part: &BackgroundEffectRule) {
|
||||
merge_clone_opt!((self, part), xray, blur);
|
||||
|
||||
if let Some(x) = part.noise {
|
||||
self.noise = Some(x.0);
|
||||
}
|
||||
|
||||
if let Some(x) = part.saturation {
|
||||
self.saturation = Some(x.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::{assert_debug_snapshot, assert_snapshot};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -51,6 +52,9 @@ pub enum Trigger {
|
||||
TouchpadScrollUp,
|
||||
TouchpadScrollLeft,
|
||||
TouchpadScrollRight,
|
||||
TabletStylusButton1,
|
||||
TabletStylusButton2,
|
||||
TabletStylusButton3,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
@@ -368,7 +372,7 @@ pub enum Action {
|
||||
#[knuffel(skip)]
|
||||
UnsetWindowUrgent(u64),
|
||||
#[knuffel(skip)]
|
||||
LoadConfigFile,
|
||||
LoadConfigFile(#[knuffel(argument)] Option<String>),
|
||||
#[knuffel(skip)]
|
||||
MruAdvance {
|
||||
direction: MruDirection,
|
||||
@@ -699,7 +703,7 @@ impl From<niri_ipc::Action> for Action {
|
||||
niri_ipc::Action::ToggleWindowUrgent { id } => Self::ToggleWindowUrgent(id),
|
||||
niri_ipc::Action::SetWindowUrgent { id } => Self::SetWindowUrgent(id),
|
||||
niri_ipc::Action::UnsetWindowUrgent { id } => Self::UnsetWindowUrgent(id),
|
||||
niri_ipc::Action::LoadConfigFile {} => Self::LoadConfigFile,
|
||||
niri_ipc::Action::LoadConfigFile { path } => Self::LoadConfigFile(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -769,7 +773,7 @@ where
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
expect_only_children(node, ctx);
|
||||
|
||||
let mut seen_keys = HashSet::new();
|
||||
let mut seen_keys: HashMap<Key, &knuffel::ast::SpannedNode<S>> = HashMap::new();
|
||||
|
||||
let mut binds = Vec::new();
|
||||
|
||||
@@ -779,39 +783,26 @@ where
|
||||
ctx.emit_error(e);
|
||||
}
|
||||
Ok(bind) => {
|
||||
if seen_keys.insert(bind.key) {
|
||||
binds.push(bind);
|
||||
} else {
|
||||
// ideally, this error should point to the previous instance of this keybind
|
||||
//
|
||||
// i (sodiboo) have tried to implement this in various ways:
|
||||
// miette!(), #[derive(Diagnostic)]
|
||||
// DecodeError::Custom, DecodeError::Conversion
|
||||
// nothing seems to work, and i suspect it's not possible.
|
||||
//
|
||||
// DecodeError is fairly restrictive.
|
||||
// even DecodeError::Custom just wraps a std::error::Error
|
||||
// and this erases all rich information from miette. (why???)
|
||||
//
|
||||
// why does knuffel do this?
|
||||
// from what i can tell, it doesn't even use DecodeError for much.
|
||||
// it only ever converts them to a Report anyways!
|
||||
// https://github.com/tailhook/knuffel/blob/c44c6b0c0f31ea6d1174d5d2ed41064922ea44ca/src/wrappers.rs#L55-L58
|
||||
//
|
||||
// besides like, allowing downstream users (such as us!)
|
||||
// to match on parse failure, i don't understand why
|
||||
// it doesn't just use a generic error type
|
||||
//
|
||||
// even the matching isn't consistent,
|
||||
// because errors can also be omitted as ctx.emit_error.
|
||||
// why does *that one* especially, require a DecodeError?
|
||||
//
|
||||
// anyways if you can make it format nicely, definitely do fix this
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&child.node_name,
|
||||
"keybind",
|
||||
"duplicate keybind",
|
||||
));
|
||||
match seen_keys.entry(bind.key) {
|
||||
Entry::Occupied(entry) => {
|
||||
// Even though it's technically incorrect, we use
|
||||
// `DecodeError::Missing` here because it labels the bind with
|
||||
// "node starts here", which is the least bad option
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
entry.get(),
|
||||
"keybind first defined here",
|
||||
));
|
||||
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&child.node_name,
|
||||
"keybind",
|
||||
"duplicate keybind later defined here",
|
||||
));
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(child);
|
||||
binds.push(bind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1012,6 +1003,12 @@ impl FromStr for Key {
|
||||
Trigger::TouchpadScrollLeft
|
||||
} else if key.eq_ignore_ascii_case("TouchpadScrollRight") {
|
||||
Trigger::TouchpadScrollRight
|
||||
} else if key.eq_ignore_ascii_case("TabletStylusButton1") {
|
||||
Trigger::TabletStylusButton1
|
||||
} else if key.eq_ignore_ascii_case("TabletStylusButton2") {
|
||||
Trigger::TabletStylusButton2
|
||||
} else if key.eq_ignore_ascii_case("TabletStylusButton3") {
|
||||
Trigger::TabletStylusButton3
|
||||
} else {
|
||||
let mut keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
|
||||
// The keyboard event handling code can receive either
|
||||
@@ -1034,7 +1031,7 @@ impl FromStr for Key {
|
||||
// [0]: https://github.com/xkbcommon/libxkbcommon/blob/45a118d5325b051343b4b174f60c1434196fa7d4/src/keysym.c#L276
|
||||
// [1]: https://docs.rs/xkbcommon/latest/xkbcommon/xkb/keysyms/index.html#:~:text=KEY%5FXF86ScreenSaver
|
||||
//
|
||||
// See https://github.com/YaLTeR/niri/issues/1969
|
||||
// See https://github.com/niri-wm/niri/issues/1969
|
||||
if keysym == Keysym::XF86_Screensaver {
|
||||
keysym = keysym_from_name(key, KEYSYM_NO_FLAGS);
|
||||
if keysym.raw() == KEY_NoSymbol {
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct Debug {
|
||||
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>,
|
||||
@@ -42,8 +41,6 @@ pub struct DebugPart {
|
||||
#[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>,
|
||||
@@ -82,7 +79,6 @@ impl MergeWith<DebugPart> for Debug {
|
||||
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,
|
||||
|
||||
@@ -364,6 +364,10 @@ pub struct Tablet {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub map_to_output: Option<String>,
|
||||
#[knuffel(child)]
|
||||
pub map_to_focused_output: bool,
|
||||
#[knuffel(child)]
|
||||
pub map_to_focused_window: bool,
|
||||
#[knuffel(child)]
|
||||
pub left_handed: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule};
|
||||
use crate::appearance::{BackgroundEffectRule, BlockOutFrom, CornerRadius, ShadowRule};
|
||||
use crate::utils::RegexEq;
|
||||
use crate::window_rule::PopupsRule;
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LayerRule {
|
||||
@@ -20,6 +21,10 @@ pub struct LayerRule {
|
||||
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)]
|
||||
@@ -28,4 +33,6 @@ pub struct Match {
|
||||
pub namespace: Option<RegexEq>,
|
||||
#[knuffel(property)]
|
||||
pub at_startup: Option<bool>,
|
||||
#[knuffel(property, str)]
|
||||
pub layer: Option<niri_ipc::Layer>,
|
||||
}
|
||||
|
||||
@@ -59,7 +59,9 @@ use crate::recent_windows::RecentWindowsPart;
|
||||
pub use crate::recent_windows::{MruDirection, MruFilter, MruPreviews, MruScope, RecentWindows};
|
||||
pub use crate::utils::FloatOrInt;
|
||||
use crate::utils::{Flag, MergeWith as _};
|
||||
pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule};
|
||||
pub use crate::window_rule::{
|
||||
FloatingPosition, PopupsRule, RelativeTo, ResolvedPopupsRules, WindowRule,
|
||||
};
|
||||
pub use crate::workspace::{Workspace, WorkspaceLayoutPart};
|
||||
|
||||
const RECURSION_LIMIT: u8 = 10;
|
||||
@@ -78,6 +80,7 @@ pub struct Config {
|
||||
pub hotkey_overlay: HotkeyOverlay,
|
||||
pub config_notification: ConfigNotification,
|
||||
pub animations: Animations,
|
||||
pub blur: Blur,
|
||||
pub gestures: Gestures,
|
||||
pub overview: Overview,
|
||||
pub environment: Environment,
|
||||
@@ -194,6 +197,7 @@ where
|
||||
"hotkey-overlay" => m_merge!(hotkey_overlay),
|
||||
"config-notification" => m_merge!(config_notification),
|
||||
"animations" => m_merge!(animations),
|
||||
"blur" => m_merge!(blur),
|
||||
"gestures" => m_merge!(gestures),
|
||||
"overview" => m_merge!(overview),
|
||||
"xwayland-satellite" => m_merge!(xwayland_satellite),
|
||||
@@ -336,12 +340,26 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
let path = base.0.join(path);
|
||||
|
||||
// We use DecodeError::Missing throughout this block because it results in the
|
||||
// least confusing error messages while still allowing to provide a span.
|
||||
|
||||
// Expand ~ into the home dir
|
||||
let path = if let Ok(rest) = path.strip_prefix("~") {
|
||||
let Some(home) = std::env::home_dir() else {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
node,
|
||||
format!("error retrieving home directory to expand {path:?}"),
|
||||
));
|
||||
continue;
|
||||
};
|
||||
|
||||
home.join(rest)
|
||||
} else {
|
||||
// Otherwise, use the current include base dir
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
base.0.join(path)
|
||||
};
|
||||
|
||||
let recursion = ctx.get::<Recursion>().unwrap().0 + 1;
|
||||
if recursion == RECURSION_LIMIT {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
@@ -701,6 +719,8 @@ mod tests {
|
||||
|
||||
tablet {
|
||||
map-to-output "eDP-1"
|
||||
map-to-focused-output
|
||||
map-to-focused-window
|
||||
calibration-matrix 1.0 2.0 3.0 \
|
||||
4.0 5.0 6.0
|
||||
}
|
||||
@@ -725,6 +745,7 @@ mod tests {
|
||||
transform "flipped-90"
|
||||
position x=10 y=20
|
||||
mode "1920x1080@144"
|
||||
max-bpc 10
|
||||
variable-refresh-rate on-demand=true
|
||||
background-color "rgba(25, 25, 102, 1.0)"
|
||||
hot-corners {
|
||||
@@ -837,7 +858,7 @@ mod tests {
|
||||
window-open { off; }
|
||||
|
||||
window-close {
|
||||
curve "cubic-bezier" 0.05 0.7 0.1 1
|
||||
curve "cubic-bezier" 0.05 0.7 0.1 1
|
||||
}
|
||||
|
||||
recent-windows-close {
|
||||
@@ -1093,6 +1114,8 @@ mod tests {
|
||||
map_to_output: Some(
|
||||
"eDP-1",
|
||||
),
|
||||
map_to_focused_output: true,
|
||||
map_to_focused_window: true,
|
||||
left_handed: false,
|
||||
},
|
||||
touch: Touch {
|
||||
@@ -1138,6 +1161,11 @@ mod tests {
|
||||
y: 20,
|
||||
},
|
||||
),
|
||||
max_bpc: Some(
|
||||
MaxBpc(
|
||||
_10,
|
||||
),
|
||||
),
|
||||
mode: Some(
|
||||
Mode {
|
||||
custom: false,
|
||||
@@ -1183,6 +1211,7 @@ mod tests {
|
||||
scale: None,
|
||||
transform: Normal,
|
||||
position: None,
|
||||
max_bpc: None,
|
||||
mode: Some(
|
||||
Mode {
|
||||
custom: true,
|
||||
@@ -1209,6 +1238,7 @@ mod tests {
|
||||
scale: None,
|
||||
transform: Normal,
|
||||
position: None,
|
||||
max_bpc: None,
|
||||
mode: None,
|
||||
modeline: Some(
|
||||
Modeline {
|
||||
@@ -1616,6 +1646,13 @@ mod tests {
|
||||
},
|
||||
),
|
||||
},
|
||||
blur: Blur {
|
||||
off: false,
|
||||
passes: 3,
|
||||
offset: 3.0,
|
||||
noise: 0.02,
|
||||
saturation: 1.5,
|
||||
},
|
||||
gestures: Gestures {
|
||||
dnd_edge_view_scroll: DndEdgeViewScroll {
|
||||
trigger_width: 10.0,
|
||||
@@ -1845,6 +1882,22 @@ mod tests {
|
||||
),
|
||||
scroll_factor: None,
|
||||
tiled_state: None,
|
||||
background_effect: BackgroundEffectRule {
|
||||
xray: None,
|
||||
blur: None,
|
||||
noise: None,
|
||||
saturation: None,
|
||||
},
|
||||
popups: PopupsRule {
|
||||
opacity: None,
|
||||
geometry_corner_radius: None,
|
||||
background_effect: BackgroundEffectRule {
|
||||
xray: None,
|
||||
blur: None,
|
||||
noise: None,
|
||||
saturation: None,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
layer_rules: [
|
||||
@@ -1859,6 +1912,7 @@ mod tests {
|
||||
),
|
||||
),
|
||||
at_startup: None,
|
||||
layer: None,
|
||||
},
|
||||
],
|
||||
excludes: [],
|
||||
@@ -1879,6 +1933,22 @@ mod tests {
|
||||
geometry_corner_radius: None,
|
||||
place_within_backdrop: None,
|
||||
baba_is_float: None,
|
||||
background_effect: BackgroundEffectRule {
|
||||
xray: None,
|
||||
blur: None,
|
||||
noise: None,
|
||||
saturation: None,
|
||||
},
|
||||
popups: PopupsRule {
|
||||
opacity: None,
|
||||
geometry_corner_radius: None,
|
||||
background_effect: BackgroundEffectRule {
|
||||
xray: None,
|
||||
blur: None,
|
||||
noise: None,
|
||||
saturation: None,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
binds: Binds(
|
||||
@@ -2182,7 +2252,6 @@ mod tests {
|
||||
enable_overlay_planes: false,
|
||||
disable_cursor_plane: false,
|
||||
disable_direct_scanout: false,
|
||||
keep_max_bpc_unchanged: false,
|
||||
restrict_primary_scanout_to_matching_format: false,
|
||||
force_disable_connectors_on_resume: false,
|
||||
render_drm_device: Some(
|
||||
|
||||
@@ -59,6 +59,8 @@ pub struct Output {
|
||||
pub transform: Transform,
|
||||
#[knuffel(child)]
|
||||
pub position: Option<Position>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub max_bpc: Option<MaxBpc>,
|
||||
#[knuffel(child)]
|
||||
pub mode: Option<Mode>,
|
||||
#[knuffel(child)]
|
||||
@@ -101,6 +103,7 @@ impl Default for Output {
|
||||
scale: None,
|
||||
transform: Transform::Normal,
|
||||
position: None,
|
||||
max_bpc: None,
|
||||
mode: None,
|
||||
modeline: None,
|
||||
variable_refresh_rate: None,
|
||||
@@ -128,6 +131,9 @@ pub struct Position {
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct MaxBpc(pub niri_ipc::MaxBpc);
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Default)]
|
||||
pub struct Vrr {
|
||||
#[knuffel(property, default = false)]
|
||||
@@ -257,6 +263,42 @@ impl OutputName {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ErrorSpan> knuffel::DecodeScalar<S> for MaxBpc {
|
||||
fn type_check(
|
||||
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
|
||||
ctx: &mut 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(
|
||||
value: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
|
||||
ctx: &mut Context<S>,
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
match &**value {
|
||||
knuffel::ast::Literal::Int(ref val) => match u8::try_from(val) {
|
||||
Ok(v) => niri_ipc::MaxBpc::try_from(v)
|
||||
.map(MaxBpc)
|
||||
.map_err(|e| DecodeError::conversion(value, e)),
|
||||
Err(e) => {
|
||||
ctx.emit_error(DecodeError::conversion(value, e));
|
||||
Ok(Self::default())
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
ctx.emit_error(DecodeError::scalar_kind(knuffel::decode::Kind::Int, value));
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use niri_ipc::ColumnDisplay;
|
||||
|
||||
use crate::appearance::{BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule};
|
||||
use crate::appearance::{
|
||||
BackgroundEffect, BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule,
|
||||
TabIndicatorRule,
|
||||
};
|
||||
use crate::layout::DefaultPresetSize;
|
||||
use crate::utils::RegexEq;
|
||||
use crate::utils::{MergeWith, RegexEq};
|
||||
use crate::FloatOrInt;
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
@@ -72,6 +75,46 @@ pub struct WindowRule {
|
||||
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)]
|
||||
|
||||
@@ -13,7 +13,7 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "1.2.0", optional = true }
|
||||
schemars = { version = "1.2.1", optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# niri-ipc
|
||||
|
||||
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
|
||||
Types and helpers for interfacing with the [niri](https://github.com/niri-wm/niri) Wayland compositor.
|
||||
|
||||
## Backwards compatibility
|
||||
|
||||
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=25.11.0"
|
||||
niri-ipc = "=26.4.0"
|
||||
```
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=25.11.0"
|
||||
//! niri-ipc = "=26.4.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -936,7 +936,13 @@ pub enum Action {
|
||||
///
|
||||
/// 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 {},
|
||||
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.
|
||||
@@ -1091,6 +1097,12 @@ pub enum OutputAction {
|
||||
#[cfg_attr(feature = "clap", command(flatten))]
|
||||
vrr: VrrToSet,
|
||||
},
|
||||
/// Set the maximum bits per channel (bit depth).
|
||||
MaxBpc {
|
||||
/// Maximum bits per channel to set.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
max_bpc: MaxBpc,
|
||||
},
|
||||
}
|
||||
|
||||
/// Output mode to set.
|
||||
@@ -1222,6 +1234,8 @@ pub struct Output {
|
||||
///
|
||||
/// `None` if the output is not mapped to any logical output (for example, if it is disabled).
|
||||
pub logical: Option<LogicalOutput>,
|
||||
/// Maximum bits per channel (bit depth), if known.
|
||||
pub max_bpc: Option<u8>,
|
||||
}
|
||||
|
||||
/// Output mode.
|
||||
@@ -1285,6 +1299,32 @@ pub enum Transform {
|
||||
Flipped270,
|
||||
}
|
||||
|
||||
/// Output maximum bits per channel.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum MaxBpc {
|
||||
/// 6-bit.
|
||||
#[serde(rename = "6")]
|
||||
_6 = 6,
|
||||
/// 8-bit.
|
||||
#[default]
|
||||
#[serde(rename = "8")]
|
||||
_8 = 8,
|
||||
/// 10-bit.
|
||||
#[serde(rename = "10")]
|
||||
_10 = 10,
|
||||
/// 12-bit.
|
||||
#[serde(rename = "12")]
|
||||
_12 = 12,
|
||||
/// 14-bit.
|
||||
#[serde(rename = "14")]
|
||||
_14 = 14,
|
||||
/// 16-bit.
|
||||
#[serde(rename = "16")]
|
||||
_16 = 16,
|
||||
}
|
||||
|
||||
/// Toplevel window.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -1862,6 +1902,44 @@ impl FromStr for Transform {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for MaxBpc {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
6 => Ok(MaxBpc::_6),
|
||||
8 => Ok(MaxBpc::_8),
|
||||
10 => Ok(MaxBpc::_10),
|
||||
12 => Ok(MaxBpc::_12),
|
||||
14 => Ok(MaxBpc::_14),
|
||||
16 => Ok(MaxBpc::_16),
|
||||
_ => Err("invalid max-bpc, can be 6, 8, 10, 12, 14, 16"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MaxBpc {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s.parse::<u8>().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ repository.workspace = true
|
||||
adw = { version = "0.8.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.11.0", path = ".." }
|
||||
niri-config = { version = "25.11.0", path = "../niri-config" }
|
||||
niri = { version = "26.4.0", path = ".." }
|
||||
niri-config = { version = "26.4.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::Duration;
|
||||
|
||||
use niri::animation::Clock;
|
||||
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options, SizingMode};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::render_helpers::{RenderCtx, RenderTarget};
|
||||
use niri_config::{Color, OutputName, PresetSize};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
@@ -270,12 +270,15 @@ impl TestCase for Layout {
|
||||
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(renderer, RenderTarget::Output, true, &mut |elem| {
|
||||
rv.push(Box::new(elem) as _)
|
||||
});
|
||||
.render_workspaces(ctx, true, &mut |elem| rv.push(Box::new(elem) as _));
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::Options;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::render_helpers::xray::XrayPos;
|
||||
use niri::render_helpers::{RenderCtx, RenderTarget};
|
||||
use niri_config::Color;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
@@ -121,13 +122,16 @@ impl TestCase for Tile {
|
||||
);
|
||||
|
||||
let mut rv = Vec::new();
|
||||
self.tile.render(
|
||||
let ctx = RenderCtx {
|
||||
renderer,
|
||||
location,
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
&mut |elem| rv.push(Box::new(elem) as _),
|
||||
);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use niri::layout::{LayoutElement, SizingMode};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::render_helpers::{RenderCtx, RenderTarget};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Scale, Size};
|
||||
@@ -53,14 +53,15 @@ impl TestCase for Window {
|
||||
.downscale(2.);
|
||||
|
||||
let mut rv = Vec::new();
|
||||
self.window.render_normal(
|
||||
let ctx = RenderCtx {
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
1.,
|
||||
RenderTarget::Output,
|
||||
&mut |elem| rv.push(Box::new(elem) as _),
|
||||
);
|
||||
target: RenderTarget::Output,
|
||||
xray: None,
|
||||
};
|
||||
self.window
|
||||
.render_normal(ctx, location, Scale::from(1.), 1., &mut |elem| {
|
||||
rv.push(Box::new(elem) as _)
|
||||
});
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ 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::*;
|
||||
@@ -206,8 +207,15 @@ 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], &[])
|
||||
.draw(&mut frame, src, dst, &[damage], &[], Some(&cache))
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use niri::layout::{
|
||||
use niri::render_helpers::offscreen::OffscreenData;
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::render_helpers::RenderCtx;
|
||||
use niri::utils::transaction::Transaction;
|
||||
use niri::window::ResolvedWindowRules;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
@@ -151,11 +151,10 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn render_normal<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
_ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
_target: RenderTarget,
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
@@ -38,7 +38,6 @@ SourceLicense: GPL-3.0-or-later
|
||||
# 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
|
||||
@@ -53,14 +52,13 @@ SourceLicense: GPL-3.0-or-later
|
||||
# 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: ((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 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 (Unlicense OR MIT) AND Zlib AND (Zlib OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/YaLTeR/niri
|
||||
URL: https://github.com/niri-wm/niri
|
||||
VCS: {{{ git_dir_vcs }}}
|
||||
Source: {{{ git_dir_pack }}}
|
||||
|
||||
|
||||
@@ -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://yalter.github.io/niri/Configuration:-Introduction
|
||||
// https://niri-wm.github.io/niri/Configuration:-Introduction
|
||||
|
||||
// Input device configuration.
|
||||
// Find the full list of options on the wiki:
|
||||
// https://yalter.github.io/niri/Configuration:-Input
|
||||
// https://niri-wm.github.io/niri/Configuration:-Input
|
||||
input {
|
||||
keyboard {
|
||||
xkb {
|
||||
@@ -73,7 +73,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://yalter.github.io/niri/Configuration:-Outputs
|
||||
// https://niri-wm.github.io/niri/Configuration:-Outputs
|
||||
// Remember to uncomment the node by removing "/-"!
|
||||
/-output "eDP-1" {
|
||||
// Uncomment this line to disable this output.
|
||||
@@ -108,7 +108,7 @@ input {
|
||||
|
||||
// Settings that influence how windows are positioned and sized.
|
||||
// Find more information on the wiki:
|
||||
// https://yalter.github.io/niri/Configuration:-Layout
|
||||
// https://niri-wm.github.io/niri/Configuration:-Layout
|
||||
layout {
|
||||
// Set gaps around windows in logical pixels.
|
||||
gaps 16
|
||||
@@ -134,7 +134,7 @@ layout {
|
||||
// fixed 1920
|
||||
}
|
||||
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Ctrl+Shift+R) toggles between.
|
||||
// preset-window-heights { }
|
||||
|
||||
// You can change the default width of the new windows.
|
||||
@@ -295,7 +295,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://yalter.github.io/niri/Configuration:-Animations
|
||||
// https://niri-wm.github.io/niri/Configuration:-Animations
|
||||
animations {
|
||||
// Uncomment to turn off all animations.
|
||||
// off
|
||||
@@ -306,7 +306,7 @@ animations {
|
||||
|
||||
// Window rules let you adjust behavior for individual windows.
|
||||
// Find more information on the wiki:
|
||||
// https://yalter.github.io/niri/Configuration:-Window-Rules
|
||||
// https://niri-wm.github.io/niri/Configuration:-Window-Rules
|
||||
|
||||
// Work around WezTerm's initial configure bug
|
||||
// by setting an empty default-column-width.
|
||||
@@ -383,6 +383,7 @@ binds {
|
||||
// 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"; }
|
||||
XF86AudioPause 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"; }
|
||||
@@ -550,11 +551,14 @@ 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+R { switch-preset-column-width-back; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Shift+R { switch-preset-column-width-back; }
|
||||
|
||||
Mod+Ctrl+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ if [ -n "$SHELL" ] &&
|
||||
! (echo "$SHELL" | grep -q "false") &&
|
||||
! (echo "$SHELL" | grep -q "nologin"); then
|
||||
if [ "$1" != '-l' ]; then
|
||||
exec bash -c "exec -l '$SHELL' -c '$0 -l $*'"
|
||||
exec bash -c "exec -l '$SHELL' -c 'exec $0 -l $*'"
|
||||
else
|
||||
shift
|
||||
fi
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::thread;
|
||||
|
||||
use accesskit::{
|
||||
ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Live, Node, NodeId, Role,
|
||||
Tree, TreeUpdate,
|
||||
Tree, TreeId, TreeUpdate,
|
||||
};
|
||||
use accesskit_unix::Adapter;
|
||||
use calloop::LoopHandle;
|
||||
@@ -220,6 +220,7 @@ impl Niri {
|
||||
let update = TreeUpdate {
|
||||
nodes,
|
||||
tree: None,
|
||||
tree_id: TreeId::ROOT,
|
||||
focus,
|
||||
};
|
||||
|
||||
@@ -246,6 +247,7 @@ impl Niri {
|
||||
let update = TreeUpdate {
|
||||
nodes: vec![(ID_ANNOUNCEMENT, node)],
|
||||
tree: None,
|
||||
tree_id: TreeId::ROOT,
|
||||
focus: self.a11y.focus,
|
||||
};
|
||||
|
||||
@@ -339,6 +341,7 @@ impl Niri {
|
||||
(ID_MRU, mru),
|
||||
],
|
||||
tree: Some(tree),
|
||||
tree_id: TreeId::ROOT,
|
||||
focus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ impl Headless {
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
max_bpc: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use anyhow::{anyhow, bail, ensure, Context};
|
||||
use bytemuck::cast_slice_mut;
|
||||
use drm_ffi::drm_mode_modeinfo;
|
||||
use libc::dev_t;
|
||||
use niri_config::output::Modeline;
|
||||
use niri_config::output::{MaxBpc, Modeline};
|
||||
use niri_config::{Config, OutputName};
|
||||
use niri_ipc::{HSyncPolarity, VSyncPolarity};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
@@ -67,10 +67,14 @@ use crate::frame_clock::FrameClock;
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::debug::draw_damage;
|
||||
use crate::render_helpers::renderer::AsGlesRenderer;
|
||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
|
||||
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation};
|
||||
|
||||
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
|
||||
const SUPPORTED_COLOR_FORMATS: [Fourcc; 8] = [
|
||||
Fourcc::Xrgb2101010,
|
||||
Fourcc::Xbgr2101010,
|
||||
Fourcc::Argb2101010,
|
||||
Fourcc::Abgr2101010,
|
||||
Fourcc::Xrgb8888,
|
||||
Fourcc::Xbgr8888,
|
||||
Fourcc::Argb8888,
|
||||
@@ -97,9 +101,6 @@ pub struct Tty {
|
||||
dmabuf_global: Option<DmabufGlobal>,
|
||||
// The output config had changed, but the session is paused, so we need to update it on resume.
|
||||
update_output_config_on_resume: bool,
|
||||
// The ignored nodes have changed, but the session is paused, so we need to update it on
|
||||
// resume.
|
||||
update_ignored_nodes_on_resume: bool,
|
||||
// Whether the debug tinting is enabled.
|
||||
debug_tint: bool,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
@@ -408,6 +409,8 @@ struct ConnectorProperties<'a> {
|
||||
device: &'a DrmDevice,
|
||||
connector: connector::Handle,
|
||||
properties: Vec<(property::Info, property::RawValue)>,
|
||||
has_change: bool,
|
||||
requests: AtomicModeReq,
|
||||
}
|
||||
|
||||
impl Tty {
|
||||
@@ -441,6 +444,14 @@ impl Tty {
|
||||
}
|
||||
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
|
||||
|
||||
// If the session is not active at startup (e.g. niri was launched from a different TTY),
|
||||
// suspend libinput now so that when ActivateSession fires, libinput.resume() performs a
|
||||
// full re-enumeration of input devices instead of being a no-op.
|
||||
if !session.is_active() {
|
||||
debug!("session is not active, starting libinput in paused state");
|
||||
libinput.suspend();
|
||||
}
|
||||
|
||||
let input_backend = LibinputInputBackend::new(libinput.clone());
|
||||
event_loop
|
||||
.insert_source(input_backend, |mut event, _, state| {
|
||||
@@ -487,11 +498,6 @@ impl Tty {
|
||||
}
|
||||
info!("using as the render node: {node_path}");
|
||||
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&config.borrow());
|
||||
if ignored_nodes.remove(&primary_node) || ignored_nodes.remove(&primary_render_node) {
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
session,
|
||||
@@ -500,17 +506,27 @@ impl Tty {
|
||||
gpu_manager,
|
||||
primary_node,
|
||||
primary_render_node,
|
||||
ignored_nodes,
|
||||
ignored_nodes: HashSet::new(),
|
||||
devices: HashMap::new(),
|
||||
dmabuf_global: None,
|
||||
update_output_config_on_resume: false,
|
||||
update_ignored_nodes_on_resume: false,
|
||||
debug_tint: false,
|
||||
ipc_outputs: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
// If the session is inactive, skip initialization because we won't be able to do much with
|
||||
// the devices anyway. We'll get ActivateSession and add the devices there instead.
|
||||
//
|
||||
// This can happen when starting niri while having a different TTY active (e.g. via tmux).
|
||||
if !self.session.is_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the ignored nodes.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
let udev = self.udev_dispatcher.clone();
|
||||
let udev = udev.as_source_ref();
|
||||
|
||||
@@ -550,6 +566,10 @@ impl Tty {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recompute ignored nodes to resolve symlinks (like /dev/dri/by-path/...) to their
|
||||
// new underlying device IDs.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
if let Err(err) = self.device_added(device_id, &path, niri) {
|
||||
warn!("error adding device: {err:?}");
|
||||
}
|
||||
@@ -597,16 +617,9 @@ impl Tty {
|
||||
warn!("error resuming libinput");
|
||||
}
|
||||
|
||||
if self.update_ignored_nodes_on_resume {
|
||||
self.update_ignored_nodes_on_resume = false;
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
||||
if ignored_nodes.remove(&self.primary_node)
|
||||
|| ignored_nodes.remove(&self.primary_render_node)
|
||||
{
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
self.ignored_nodes = ignored_nodes;
|
||||
}
|
||||
// While the session was suspended, GPUs could have been added, so
|
||||
// /dev/dri/by-path/... symlinks need to be re-resolved.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
let mut device_list = self
|
||||
.udev_dispatcher
|
||||
@@ -649,7 +662,7 @@ impl Tty {
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
|
||||
// Someone on an old device hit what seems to be a driver bug without this:
|
||||
// https://github.com/YaLTeR/niri/issues/3048
|
||||
// https://github.com/niri-wm/niri/issues/3048
|
||||
let force_disable = self
|
||||
.config
|
||||
.borrow()
|
||||
@@ -669,16 +682,19 @@ impl Tty {
|
||||
// Apply pending gamma changes and restore our existing gamma.
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
for (crtc, surface) in device.surfaces.iter_mut() {
|
||||
if let Ok(props) =
|
||||
if let Ok(mut props) =
|
||||
ConnectorProperties::try_new(&device.drm, surface.connector)
|
||||
{
|
||||
match reset_hdr(&props) {
|
||||
Ok(()) => (),
|
||||
Err(err) => debug!("couldn't reset HDR properties: {err:?}"),
|
||||
}
|
||||
let max_bpc = self
|
||||
.config
|
||||
.borrow()
|
||||
.outputs
|
||||
.find(&surface.name)
|
||||
.and_then(|o| o.max_bpc);
|
||||
set_connector_properties(&mut props, max_bpc, true);
|
||||
} else {
|
||||
warn!("failed to get connector properties");
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(ramp) = surface.pending_gamma_change.take() {
|
||||
let ramp = ramp.as_deref();
|
||||
@@ -699,7 +715,14 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Add new devices.
|
||||
for (device_id, path) in device_list.into_iter() {
|
||||
//
|
||||
// Add the primary node first as later nodes might depend on the primary render
|
||||
// node being available.
|
||||
let primary_device_id = self.primary_node.dev_id();
|
||||
let primary_device_path = device_list.remove(&primary_device_id);
|
||||
let primary = primary_device_path.map(|path| (primary_device_id, path));
|
||||
|
||||
for (device_id, path) in primary.into_iter().chain(device_list) {
|
||||
if let Err(err) = self.device_added(device_id, &path, niri) {
|
||||
warn!("error adding device: {err:?}");
|
||||
}
|
||||
@@ -809,7 +832,10 @@ impl Tty {
|
||||
.context("error creating renderer")?;
|
||||
|
||||
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||
warn!("error binding wl-display in EGL: {err:?}");
|
||||
// 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");
|
||||
}
|
||||
|
||||
let gles_renderer = renderer.as_gles_renderer();
|
||||
@@ -979,6 +1005,32 @@ impl Tty {
|
||||
} => {
|
||||
removed.push(crtc);
|
||||
}
|
||||
// Emitted when the list of connector modes changes at runtime.
|
||||
//
|
||||
// Some devices, notably USB-C docks with DP-MST/alt-mode, report Connected before
|
||||
// the EDID has been read, with an empty mode list. Then, at a later point, the
|
||||
// modes will be populated, at which point we'll get this Changed event.
|
||||
DrmScanEvent::Changed {
|
||||
connector,
|
||||
crtc: Some(crtc),
|
||||
} => {
|
||||
let connector_name = format_connector_name(&connector);
|
||||
let name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||
debug!(
|
||||
"connector changed: {} \"{}\"",
|
||||
&name.connector,
|
||||
name.format_make_model_serial(),
|
||||
);
|
||||
|
||||
if !device.known_crtcs.contains_key(&crtc) {
|
||||
// I guess this can happen if the connector initially wasn't mapped to a
|
||||
// CRTC but then got mapped before being changed.
|
||||
warn!("changed connector missing from known crtcs");
|
||||
}
|
||||
|
||||
// We don't actually need to do anything here; on_output_config_changed() will
|
||||
// take care of picking a new mode if needed.
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -1259,13 +1311,10 @@ impl Tty {
|
||||
debug!("picking mode: {mode:?}");
|
||||
|
||||
let mut orientation = None;
|
||||
if let Ok(props) = ConnectorProperties::try_new(&device.drm, connector.handle()) {
|
||||
match reset_hdr(&props) {
|
||||
Ok(()) => (),
|
||||
Err(err) => debug!("couldn't reset HDR properties: {err:?}"),
|
||||
}
|
||||
if let Ok(mut props) = ConnectorProperties::try_new(&device.drm, connector.handle()) {
|
||||
set_connector_properties(&mut props, config.max_bpc, true);
|
||||
|
||||
match get_panel_orientation(&props) {
|
||||
match props.get_panel_orientation() {
|
||||
Ok(x) => orientation = Some(x),
|
||||
Err(err) => {
|
||||
trace!("couldn't get panel orientation: {err:?}");
|
||||
@@ -1273,7 +1322,7 @@ impl Tty {
|
||||
}
|
||||
} else {
|
||||
warn!("failed to get connector properties");
|
||||
};
|
||||
}
|
||||
|
||||
let mut gamma_props = GammaProps::new(&device.drm, crtc)
|
||||
.map_err(|err| debug!("couldn't get gamma properties: {err:?}"))
|
||||
@@ -1393,7 +1442,7 @@ impl Tty {
|
||||
|
||||
// Create the compositor.
|
||||
let res = DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
OutputModeSource::Auto(output.downgrade()),
|
||||
surface,
|
||||
None,
|
||||
device.allocator.clone(),
|
||||
@@ -1423,7 +1472,7 @@ impl Tty {
|
||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
|
||||
DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
OutputModeSource::Auto(output.downgrade()),
|
||||
surface,
|
||||
None,
|
||||
device.allocator.clone(),
|
||||
@@ -1677,8 +1726,8 @@ impl Tty {
|
||||
// This is an error!() because it shouldn't happen, but on some systems it somehow
|
||||
// does. Kernel sending rogue vblank events?
|
||||
//
|
||||
// https://github.com/YaLTeR/niri/issues/556
|
||||
// https://github.com/YaLTeR/niri/issues/615
|
||||
// https://github.com/niri-wm/niri/issues/556
|
||||
// https://github.com/niri-wm/niri/issues/615
|
||||
error!(
|
||||
"unexpected redraw state for output {name} (should be WaitingForVBlank); \
|
||||
can happen when resuming from sleep or powering on monitors: {state:?}"
|
||||
@@ -1839,8 +1888,12 @@ impl Tty {
|
||||
};
|
||||
|
||||
// Render the elements.
|
||||
let mut elements =
|
||||
niri.render::<TtyRenderer>(&mut renderer, output, true, RenderTarget::Output);
|
||||
let ctx = RenderCtx {
|
||||
renderer: &mut renderer,
|
||||
target: RenderTarget::Output,
|
||||
xray: None,
|
||||
};
|
||||
let mut elements = niri.render_to_vec(ctx, output, true);
|
||||
|
||||
// Visualize the damage, if enabled.
|
||||
if niri.debug_draw_damage {
|
||||
@@ -2147,6 +2200,15 @@ impl Tty {
|
||||
OutputId::next()
|
||||
});
|
||||
|
||||
let props = ConnectorProperties::try_new(&device.drm, connector.handle()).ok();
|
||||
let max_bpc = props.as_ref().and_then(|p| p.find(c"max bpc").ok());
|
||||
let max_bpc = max_bpc.and_then(|(info, value)| {
|
||||
info.value_type()
|
||||
.convert_value(*value)
|
||||
.as_unsigned_range()
|
||||
.map(|v| v as u8)
|
||||
});
|
||||
|
||||
let ipc_output = niri_ipc::Output {
|
||||
name: connector_name,
|
||||
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
|
||||
@@ -2159,6 +2221,7 @@ impl Tty {
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
max_bpc,
|
||||
};
|
||||
|
||||
ipc_outputs.insert(id, ipc_output);
|
||||
@@ -2236,22 +2299,25 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
|
||||
|
||||
// If we're inactive, we can't do anything, so just set a flag for later.
|
||||
if !self.session.is_active() {
|
||||
self.update_ignored_nodes_on_resume = true;
|
||||
return;
|
||||
}
|
||||
|
||||
fn compute_ignored_nodes(&self) -> HashSet<DrmNode> {
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
||||
if ignored_nodes.remove(&self.primary_node)
|
||||
|| ignored_nodes.remove(&self.primary_render_node)
|
||||
{
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
ignored_nodes
|
||||
}
|
||||
|
||||
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
|
||||
|
||||
// If we're inactive, we can't do anything, but we'll recompute in ActivateSession.
|
||||
if !self.session.is_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ignored_nodes = self.compute_ignored_nodes();
|
||||
if ignored_nodes == self.ignored_nodes {
|
||||
return;
|
||||
}
|
||||
@@ -2372,6 +2438,13 @@ impl Tty {
|
||||
},
|
||||
};
|
||||
|
||||
if let Ok(mut props) = ConnectorProperties::try_new(&device.drm, surface.connector)
|
||||
{
|
||||
set_connector_properties(&mut props, config.max_bpc, false);
|
||||
} else {
|
||||
warn!("failed to get connector properties");
|
||||
}
|
||||
|
||||
let change_mode = surface.compositor.pending_mode() != mode;
|
||||
|
||||
let vrr_enabled = surface.compositor.vrr_enabled();
|
||||
@@ -3200,6 +3273,8 @@ impl<'a> ConnectorProperties<'a> {
|
||||
device,
|
||||
connector,
|
||||
properties,
|
||||
has_change: false,
|
||||
requests: AtomicModeReq::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3212,35 +3287,115 @@ impl<'a> ConnectorProperties<'a> {
|
||||
|
||||
Err(anyhow!("couldn't find property: {name:?}"))
|
||||
}
|
||||
|
||||
fn get_panel_orientation(&self) -> anyhow::Result<Transform> {
|
||||
let (info, value) = self.find(c"panel orientation")?;
|
||||
match info.value_type().convert_value(*value) {
|
||||
property::Value::Enum(Some(val)) => match val.value() {
|
||||
// "Normal"
|
||||
0 => Ok(Transform::Normal),
|
||||
// "Upside Down"
|
||||
1 => Ok(Transform::_180),
|
||||
// "Left Side Up"
|
||||
2 => Ok(Transform::_90),
|
||||
// "Right Side Up"
|
||||
3 => Ok(Transform::_270),
|
||||
_ => bail!("panel orientation has invalid value: {:?}", val),
|
||||
},
|
||||
_ => bail!("panel orientation has wrong value type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_hdr(&mut self) -> anyhow::Result<()> {
|
||||
const DRM_MODE_COLORIMETRY_DEFAULT: u64 = 0;
|
||||
|
||||
let (info, value) = self.find(c"HDR_OUTPUT_METADATA")?;
|
||||
|
||||
let property::ValueType::Blob = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
if *value != 0 {
|
||||
self.requests
|
||||
.add_raw_property(self.connector.into(), info.handle(), 0);
|
||||
self.has_change = true;
|
||||
}
|
||||
|
||||
let (info, value) = self.find(c"Colorspace")?;
|
||||
let property::ValueType::Enum(_) = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
if *value != DRM_MODE_COLORIMETRY_DEFAULT {
|
||||
self.requests.add_raw_property(
|
||||
self.connector.into(),
|
||||
info.handle(),
|
||||
DRM_MODE_COLORIMETRY_DEFAULT,
|
||||
);
|
||||
self.has_change = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_max_bpc(&mut self, max_bpc: MaxBpc) -> anyhow::Result<u64> {
|
||||
let (info, value) = self.find(c"max bpc")?;
|
||||
|
||||
let property::ValueType::UnsignedRange(min, max) = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
|
||||
let max_bpc = max_bpc.0 as u64;
|
||||
if !(min..=max).contains(&max_bpc) {
|
||||
bail!("max-bpc {max_bpc} outside valid range of [{min}, {max}]");
|
||||
}
|
||||
|
||||
let property::Value::UnsignedRange(value) = info.value_type().convert_value(*value) else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
|
||||
if value != max_bpc {
|
||||
self.requests.add_raw_property(
|
||||
self.connector.into(),
|
||||
info.handle(),
|
||||
property::Value::UnsignedRange(max_bpc).into(),
|
||||
);
|
||||
self.has_change = true;
|
||||
}
|
||||
|
||||
Ok(max_bpc)
|
||||
}
|
||||
|
||||
fn commit(&mut self) -> anyhow::Result<()> {
|
||||
if self.has_change {
|
||||
self.device.atomic_commit(
|
||||
AtomicCommitFlags::ALLOW_MODESET,
|
||||
std::mem::take(&mut self.requests),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const DRM_MODE_COLORIMETRY_DEFAULT: u64 = 0;
|
||||
|
||||
fn reset_hdr(props: &ConnectorProperties) -> anyhow::Result<()> {
|
||||
let (info, value) = props.find(c"HDR_OUTPUT_METADATA")?;
|
||||
let property::ValueType::Blob = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
|
||||
if *value != 0 {
|
||||
props
|
||||
.device
|
||||
.set_property(props.connector, info.handle(), 0)
|
||||
.context("error setting property")?;
|
||||
fn set_connector_properties(
|
||||
props: &mut ConnectorProperties,
|
||||
max_bpc: Option<MaxBpc>,
|
||||
reset_hdr: bool,
|
||||
) {
|
||||
if let Some(max_bpc) = max_bpc {
|
||||
if let Err(err) = props.set_max_bpc(max_bpc) {
|
||||
debug!("failed to set `max bpc` property: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
let (info, value) = props.find(c"Colorspace")?;
|
||||
let property::ValueType::Enum(_) = info.value_type() else {
|
||||
bail!("wrong property type")
|
||||
};
|
||||
if *value != DRM_MODE_COLORIMETRY_DEFAULT {
|
||||
props
|
||||
.device
|
||||
.set_property(props.connector, info.handle(), DRM_MODE_COLORIMETRY_DEFAULT)
|
||||
.context("error setting property")?;
|
||||
if reset_hdr {
|
||||
if let Err(err) = props.reset_hdr() {
|
||||
debug!("failed to set HDR properties: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
if let Err(err) = props.commit() {
|
||||
warn!("failed to atomically commit properties: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bool> {
|
||||
@@ -3248,24 +3403,6 @@ fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bo
|
||||
info.value_type().convert_value(value).as_boolean()
|
||||
}
|
||||
|
||||
fn get_panel_orientation(props: &ConnectorProperties) -> anyhow::Result<Transform> {
|
||||
let (info, value) = props.find(c"panel orientation")?;
|
||||
match info.value_type().convert_value(*value) {
|
||||
property::Value::Enum(Some(val)) => match val.value() {
|
||||
// "Normal"
|
||||
0 => Ok(Transform::Normal),
|
||||
// "Upside Down"
|
||||
1 => Ok(Transform::_180),
|
||||
// "Left Side Up"
|
||||
2 => Ok(Transform::_90),
|
||||
// "Right Side Up"
|
||||
3 => Ok(Transform::_270),
|
||||
_ => bail!("panel orientation has invalid value: {:?}", val),
|
||||
},
|
||||
_ => bail!("panel orientation has wrong value type"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_gamma_for_crtc(
|
||||
device: &DrmDevice,
|
||||
crtc: crtc::Handle,
|
||||
|
||||
@@ -4,8 +4,10 @@ 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};
|
||||
@@ -14,13 +16,15 @@ 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, RenderTarget};
|
||||
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
|
||||
pub struct Winit {
|
||||
@@ -28,6 +32,7 @@ pub struct Winit {
|
||||
output: Output,
|
||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
dmabuf_global: Option<DmabufGlobal>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
@@ -41,7 +46,8 @@ impl Winit {
|
||||
let builder = Window::default_attributes()
|
||||
.with_inner_size(LogicalSize::new(1280.0, 800.0))
|
||||
// .with_resizable(false)
|
||||
.with_title("niri");
|
||||
.with_title("niri")
|
||||
.with_name("niri", "");
|
||||
let (backend, winit) = winit::init_from_attributes(builder)?;
|
||||
|
||||
let output = Output::new(
|
||||
@@ -89,6 +95,7 @@ impl Winit {
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
max_bpc: None,
|
||||
},
|
||||
)])));
|
||||
|
||||
@@ -135,6 +142,7 @@ impl Winit {
|
||||
output,
|
||||
backend,
|
||||
damage_tracker,
|
||||
dmabuf_global: None,
|
||||
ipc_outputs,
|
||||
})
|
||||
}
|
||||
@@ -142,7 +150,10 @@ 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) {
|
||||
warn!("error binding renderer wl_display: {err}");
|
||||
// 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");
|
||||
}
|
||||
|
||||
resources::init(renderer);
|
||||
@@ -162,9 +173,44 @@ 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()
|
||||
}
|
||||
@@ -180,12 +226,12 @@ impl Winit {
|
||||
let _span = tracy_client::span!("Winit::render");
|
||||
|
||||
// Render the elements.
|
||||
let mut elements = niri.render::<GlesRenderer>(
|
||||
self.backend.renderer(),
|
||||
output,
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
);
|
||||
let ctx = RenderCtx {
|
||||
renderer: self.backend.renderer(),
|
||||
target: RenderTarget::Output,
|
||||
xray: None,
|
||||
};
|
||||
let mut elements = niri.render_to_vec(ctx, output, true);
|
||||
|
||||
// Visualize the damage, if enabled.
|
||||
if niri.debug_draw_damage {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use smithay::delegate_background_effect;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Rectangle};
|
||||
use smithay::wayland::background_effect::{
|
||||
self, BackgroundEffectSurfaceCachedState, ExtBackgroundEffectHandler,
|
||||
};
|
||||
use smithay::wayland::compositor::{
|
||||
add_post_commit_hook, with_states, RegionAttributes, SurfaceData,
|
||||
};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::utils::region::region_to_non_overlapping_rects;
|
||||
|
||||
/// Per-surface cache for processed blur region (non-overlapping rects).
|
||||
#[derive(Default)]
|
||||
struct CachedBlurRegionUserData(Mutex<CachedBlurRegionInner>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct CachedBlurRegionInner {
|
||||
/// Whether a region change is pending to be committed.
|
||||
pending_dirty: bool,
|
||||
/// Whether the region must be recomputed.
|
||||
dirty: bool,
|
||||
/// Whether the post-commit hook has been registered for this surface.
|
||||
hook_registered: bool,
|
||||
/// Cached non-overlapping rects in surface-local coordinates.
|
||||
///
|
||||
/// `None` means there's no blur region.
|
||||
rects: Option<Arc<Vec<Rectangle<i32, Logical>>>>,
|
||||
}
|
||||
|
||||
/// Gets the cached blur region for a surface, lazily recomputing if dirty.
|
||||
pub fn get_cached_blur_region(states: &SurfaceData) -> Option<Arc<Vec<Rectangle<i32, Logical>>>> {
|
||||
let cache = states
|
||||
.data_map
|
||||
.get_or_insert_threadsafe(CachedBlurRegionUserData::default);
|
||||
let mut guard = cache.0.lock().unwrap();
|
||||
|
||||
if guard.dirty {
|
||||
guard.dirty = false;
|
||||
recompute_blur_region(states, &mut guard);
|
||||
}
|
||||
|
||||
guard.rects.clone()
|
||||
}
|
||||
|
||||
fn recompute_blur_region(states: &SurfaceData, inner: &mut CachedBlurRegionInner) {
|
||||
let cached = &states.cached_state;
|
||||
|
||||
let rects = if let Some(arc) = &mut inner.rects {
|
||||
if Arc::strong_count(arc) > 1 {
|
||||
debug!("cloning rects due to non-unique reference");
|
||||
}
|
||||
arc
|
||||
} else {
|
||||
inner.rects.insert(Arc::new(Vec::new()))
|
||||
};
|
||||
let rects = Arc::make_mut(rects);
|
||||
|
||||
if cached.has::<BackgroundEffectSurfaceCachedState>() {
|
||||
let mut guard = cached.get::<BackgroundEffectSurfaceCachedState>();
|
||||
if let Some(region) = &guard.current().blur_region {
|
||||
region_to_non_overlapping_rects(region, rects);
|
||||
} else {
|
||||
inner.rects = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
inner.rects = None;
|
||||
}
|
||||
|
||||
fn mark_blur_region_pending_dirty(wl_surface: &WlSurface) {
|
||||
let register_hook = with_states(wl_surface, |states| {
|
||||
let cache = states
|
||||
.data_map
|
||||
.get_or_insert_threadsafe(CachedBlurRegionUserData::default);
|
||||
let mut guard = cache.0.lock().unwrap();
|
||||
guard.pending_dirty = true;
|
||||
|
||||
if guard.hook_registered {
|
||||
false
|
||||
} else {
|
||||
guard.hook_registered = true;
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if register_hook {
|
||||
add_post_commit_hook::<State, _>(wl_surface, |_state, _dh, surface| {
|
||||
with_states(surface, |states| {
|
||||
if let Some(cache) = states.data_map.get::<CachedBlurRegionUserData>() {
|
||||
let mut guard = cache.0.lock().unwrap();
|
||||
if guard.pending_dirty {
|
||||
guard.pending_dirty = false;
|
||||
guard.dirty = true;
|
||||
|
||||
crate::render_helpers::background_effect::damage_surface(states);
|
||||
}
|
||||
} else {
|
||||
error!("unexpected missing CachedBlurRegionUserData");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtBackgroundEffectHandler for State {
|
||||
fn capabilities(&self) -> background_effect::Capability {
|
||||
background_effect::Capability::Blur
|
||||
}
|
||||
|
||||
fn set_blur_region(&mut self, wl_surface: WlSurface, _region: RegionAttributes) {
|
||||
mark_blur_region_pending_dirty(&wl_surface);
|
||||
}
|
||||
|
||||
fn unset_blur_region(&mut self, wl_surface: WlSurface) {
|
||||
mark_blur_region_pending_dirty(&wl_surface);
|
||||
}
|
||||
}
|
||||
delegate_background_effect!(State);
|
||||
@@ -195,7 +195,10 @@ impl CompositorHandler for State {
|
||||
// The mapped pre-commit hook deals with dma-bufs on its own.
|
||||
self.remove_default_dmabuf_pre_commit_hook(surface);
|
||||
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
|
||||
let mapped = Mapped::new(window, rules, hook);
|
||||
let mapped = {
|
||||
let config = self.niri.config.borrow();
|
||||
Mapped::new(window, rules, hook, &config)
|
||||
};
|
||||
let window = mapped.window.clone();
|
||||
|
||||
let target = if let Some(p) = &parent {
|
||||
@@ -486,11 +489,10 @@ impl CompositorHandler for State {
|
||||
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
|
||||
// gets most of the job done.
|
||||
if let Some(root) = self.niri.root_surface.get(surface) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
|
||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(root) {
|
||||
let window = mapped.window.clone();
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||
});
|
||||
let output = output.cloned();
|
||||
self.store_unmap_snapshot(&window, output.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +500,16 @@ impl CompositorHandler for State {
|
||||
.root_surface
|
||||
.retain(|k, v| k != surface && v != surface);
|
||||
|
||||
self.niri.dmabuf_pre_commit_hook.remove(surface);
|
||||
// The object destruction order is not guaranteed to follow the logical role order. So for
|
||||
// example when a client disconnects unexpectedly, WlSurface::destroyed() may be called
|
||||
// before XdgShellHandler::toplevel_destroyed(). In this case, the surface will *not* have
|
||||
// the default dmabuf pre-commit hook: it will still have the toplevel pre-commit hook.
|
||||
//
|
||||
// So, this may come out empty, and then the toplevel pre-commit hook will be removed in the
|
||||
// subsequent toplevel_destroyed() call.
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,6 +528,11 @@ delegate_shm!(State);
|
||||
|
||||
impl State {
|
||||
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if !surface.is_alive() {
|
||||
error!("tried to add dmabuf pre-commit hook for a dead surface");
|
||||
return;
|
||||
}
|
||||
|
||||
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
@@ -556,13 +572,13 @@ impl State {
|
||||
let s = surface.clone();
|
||||
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
|
||||
error!("tried to add dmabuf pre-commit hook when there was already one");
|
||||
remove_pre_commit_hook(surface, prev);
|
||||
remove_pre_commit_hook(surface, &prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
} else {
|
||||
error!("tried to remove dmabuf pre-commit hook but there was none");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::compositor::{add_pre_commit_hook, get_parent, with_states, HookId};
|
||||
use smithay::wayland::shell::wlr_layer::{
|
||||
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
WlrLayerShellState,
|
||||
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceCachedState, LayerSurfaceData,
|
||||
WlrLayerShellHandler, WlrLayerShellState,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
@@ -27,7 +26,7 @@ impl WlrLayerShellHandler for State {
|
||||
namespace: String,
|
||||
) {
|
||||
let output = if let Some(wl_output) = &wl_output {
|
||||
Output::from_resource(wl_output)
|
||||
self.niri.output_from_resource(wl_output)
|
||||
} else {
|
||||
self.niri.layout.active_output().cloned()
|
||||
};
|
||||
@@ -126,8 +125,10 @@ impl State {
|
||||
let output_size = output_size(&output);
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
|
||||
let hook = add_mapped_layer_pre_commit_hook(layer);
|
||||
let mapped = MappedLayer::new(
|
||||
layer.clone(),
|
||||
hook,
|
||||
rules,
|
||||
output_size,
|
||||
scale,
|
||||
@@ -142,6 +143,21 @@ impl State {
|
||||
if prev.is_some() {
|
||||
error!("MappedLayer was present for an unmapped surface");
|
||||
}
|
||||
} else {
|
||||
// The surface remains mapped.
|
||||
if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) {
|
||||
// Check if the layer changed.
|
||||
if mapped.take_recompute_rules_on_commit() {
|
||||
let config = self.niri.config.borrow();
|
||||
if mapped
|
||||
.recompute_layer_rules(&config.layer_rules, self.niri.is_at_startup)
|
||||
{
|
||||
mapped.update_config(&config);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("MappedLayer missing for a mapped surface");
|
||||
}
|
||||
}
|
||||
|
||||
// Give focus to newly mapped on-demand surfaces. Some launchers like lxqt-runner rely
|
||||
@@ -155,7 +171,7 @@ impl State {
|
||||
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand surfaces in
|
||||
// update_keyboard_focus(), so we don't need to check for that here.
|
||||
//
|
||||
// https://github.com/YaLTeR/niri/issues/641
|
||||
// https://github.com/niri-wm/niri/issues/641
|
||||
let on_demand = layer.cached_state().keyboard_interactivity
|
||||
== wlr_layer::KeyboardInteractivity::OnDemand;
|
||||
if was_unmapped && on_demand {
|
||||
@@ -204,3 +220,23 @@ impl State {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn add_mapped_layer_pre_commit_hook(layer: &LayerSurface) -> HookId {
|
||||
add_pre_commit_hook::<State, _>(layer.wl_surface(), move |state, _dh, surface| {
|
||||
let layer_changed = with_states(surface, |states| {
|
||||
let mut guard = states.cached_state.get::<LayerSurfaceCachedState>();
|
||||
let pending_layer = guard.pending().layer;
|
||||
let current_layer = guard.current().layer;
|
||||
pending_layer != current_layer
|
||||
});
|
||||
|
||||
if layer_changed {
|
||||
for mapped in state.niri.mapped_layer_surfaces.values_mut() {
|
||||
if mapped.surface().wl_surface() == surface {
|
||||
mapped.set_recompute_rules_on_commit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod background_effect;
|
||||
mod compositor;
|
||||
mod layer_shell;
|
||||
mod xdg_shell;
|
||||
@@ -68,7 +69,7 @@ use smithay::{
|
||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||
delegate_viewporter, delegate_xdg_activation,
|
||||
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||
};
|
||||
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
@@ -279,6 +280,7 @@ impl KeyboardShortcutsInhibitHandler for State {
|
||||
|
||||
delegate_input_method_manager!(State);
|
||||
delegate_keyboard_shortcuts_inhibit!(State);
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
|
||||
impl SelectionHandler for State {
|
||||
type SelectionUserData = Arc<[u8]>;
|
||||
@@ -359,7 +361,7 @@ impl DndGrabHandler for State {
|
||||
trace!("dnd dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// End DnD before activating a specific window below so that it takes precedence.
|
||||
self.niri.layout.dnd_end();
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
|
||||
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
|
||||
// example. On successful drop, additionally activate the target window.
|
||||
@@ -381,10 +383,21 @@ impl DndGrabHandler for State {
|
||||
self.niri.layout.focus_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
|
||||
trace!("dnd cancelled");
|
||||
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::niri::Niri {
|
||||
fn on_maybe_dnd_ended(&mut self) {
|
||||
self.layout.dnd_end();
|
||||
self.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
self.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,7 +472,7 @@ impl SessionLockHandler for State {
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
let Some(output) = Output::from_resource(&output) else {
|
||||
let Some(output) = self.niri.output_from_resource(&output) else {
|
||||
warn!("no Output matching WlOutput");
|
||||
return;
|
||||
};
|
||||
@@ -544,7 +557,9 @@ impl ForeignToplevelHandler for State {
|
||||
{
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if let Some(requested_output) =
|
||||
wl_output.and_then(|o| self.niri.output_from_resource(&o))
|
||||
{
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
@@ -620,6 +635,12 @@ delegate_ext_workspace!(State);
|
||||
|
||||
impl ScreencopyHandler for State {
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
// This can happen if the output was removed before this was called.
|
||||
if !self.niri.output_exists(screencopy.output()) {
|
||||
trace!("screencopy output no longer exists");
|
||||
return;
|
||||
}
|
||||
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
|
||||
@@ -612,7 +612,7 @@ impl XdgShellHandler for State {
|
||||
toplevel: ToplevelSurface,
|
||||
wl_output: Option<wl_output::WlOutput>,
|
||||
) {
|
||||
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
|
||||
let requested_output = wl_output.and_then(|o| self.niri.output_from_resource(&o));
|
||||
|
||||
if let Some((mapped, current_output)) = self
|
||||
.niri
|
||||
@@ -846,9 +846,7 @@ impl XdgShellHandler for State {
|
||||
self.niri
|
||||
.stop_casts_for_target(CastTarget::Window { id: id.get() });
|
||||
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||
});
|
||||
self.store_unmap_snapshot(&window, output.as_ref());
|
||||
|
||||
let transaction = Transaction::new();
|
||||
let blocker = transaction.blocker();
|
||||
@@ -863,7 +861,15 @@ impl XdgShellHandler for State {
|
||||
|
||||
self.niri.window_mru_ui.remove_window(id);
|
||||
self.niri.layout.remove_window(&window, transaction.clone());
|
||||
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
|
||||
|
||||
let surface = surface.wl_surface();
|
||||
// This check is necessary because implicit resource destruction is done with
|
||||
// undefined order, so the surface might get destroyed before toplevel_destroyed() is
|
||||
// called. In this case, adding the default pre-commit hook here would leak it, since the
|
||||
// place that removes it is WlSurface::destroyed(), which had already been called by now.
|
||||
if surface.is_alive() {
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
}
|
||||
|
||||
// If this is the only instance, then this transaction will complete immediately, so no
|
||||
// need to set the timer.
|
||||
@@ -1437,7 +1443,7 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
|
||||
let span =
|
||||
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
|
||||
|
||||
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
|
||||
let Some((mapped, output)) = state.niri.layout.find_window_and_output_mut(surface) else {
|
||||
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
|
||||
return;
|
||||
};
|
||||
@@ -1539,9 +1545,8 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
|
||||
|
||||
let window = mapped.window.clone();
|
||||
if got_unmapped {
|
||||
state.backend.with_primary_renderer(|renderer| {
|
||||
state.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||
});
|
||||
let output = output.cloned();
|
||||
state.store_unmap_snapshot(&window, output.as_ref());
|
||||
} else {
|
||||
if animate {
|
||||
state.backend.with_primary_renderer(|renderer| {
|
||||
|
||||
@@ -4,7 +4,6 @@ use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::protocols::virtual_keyboard::VirtualKeyboard;
|
||||
use crate::protocols::virtual_pointer::VirtualPointer;
|
||||
|
||||
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
|
||||
@@ -45,12 +44,6 @@ impl NiriInputDevice for WinitVirtualDevice {
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualKeyboard {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualPointer {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
self.output().cloned()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::any::Any;
|
||||
use std::cmp::min;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
@@ -7,7 +6,7 @@ use std::time::Duration;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use input::event::gesture::GestureEventCoordinates as _;
|
||||
use niri_config::{
|
||||
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger, Xkb,
|
||||
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger,
|
||||
};
|
||||
use niri_ipc::LayoutSwitchTarget;
|
||||
use smithay::backend::input::{
|
||||
@@ -41,6 +40,8 @@ use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
|
||||
use touch_overview_grab::TouchOverviewGrab;
|
||||
|
||||
use self::move_grab::MoveGrab;
|
||||
use self::pick_color_grab::PickColorGrab;
|
||||
use self::pick_window_grab::PickWindowGrab;
|
||||
use self::resize_grab::ResizeGrab;
|
||||
use self::spatial_movement_grab::SpatialMovementGrab;
|
||||
#[cfg(feature = "dbus")]
|
||||
@@ -48,7 +49,6 @@ use crate::dbus::freedesktop_a11y::KbMonBlock;
|
||||
use crate::layout::scrolling::ScrollDirection;
|
||||
use crate::layout::{ActivateWindow, LayoutElement as _};
|
||||
use crate::niri::{CastTarget, PointerVisibility, State};
|
||||
use crate::protocols::virtual_keyboard::VirtualKeyboard;
|
||||
use crate::ui::mru::{WindowMru, WindowMruUi};
|
||||
use crate::ui::screenshot_ui::ScreenshotUi;
|
||||
use crate::utils::spawning::{spawn, spawn_sh};
|
||||
@@ -294,42 +294,70 @@ impl State {
|
||||
I::Device: 'static,
|
||||
{
|
||||
let device_output = event.device().output(self);
|
||||
let device_output = device_output.filter(|output| self.niri.output_exists(output));
|
||||
let device_output = device_output.as_ref();
|
||||
let (target_geo, keep_ratio, px, transform) =
|
||||
if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) {
|
||||
(
|
||||
self.niri.global_space.output_geometry(output).unwrap(),
|
||||
true,
|
||||
1. / output.current_scale().fractional_scale(),
|
||||
output.current_transform(),
|
||||
)
|
||||
} else {
|
||||
let geo = self.global_bounding_rectangle()?;
|
||||
let mapped_output = device_output.or_else(|| self.niri.output_for_tablet());
|
||||
|
||||
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
|
||||
// corresponding to the position on the right when clamping.
|
||||
let output = self.niri.global_space.outputs().next().unwrap();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
// If the tablet is configured to map to the focused window, use that window's geometry on
|
||||
// the mapped output (or on the focused output if no specific output is mapped).
|
||||
let map_to_focused_window = self.niri.config.borrow().input.tablet.map_to_focused_window;
|
||||
// But only if the keyboard focus is on the layout, so that it doesn't trigger on the lock
|
||||
// screen and such.
|
||||
let window_target = if map_to_focused_window && self.niri.keyboard_focus.is_layout() {
|
||||
let output = mapped_output.or_else(|| self.niri.layout.active_output());
|
||||
output.and_then(|output| {
|
||||
let monitor = self.niri.layout.monitor_for_output(output)?;
|
||||
let mut rect = monitor.active_window_visual_rectangle()?;
|
||||
let output_geo = self.niri.global_space.output_geometry(output)?;
|
||||
rect.loc += output_geo.loc.to_f64();
|
||||
Some((rect, output))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
|
||||
(geo, false, 1. / scale, Transform::Normal)
|
||||
};
|
||||
let (target_geo, keep_ratio, px, transform) = if let Some((rect, output)) = window_target {
|
||||
(
|
||||
rect,
|
||||
true,
|
||||
1. / output.current_scale().fractional_scale(),
|
||||
output.current_transform(),
|
||||
)
|
||||
} else if let Some(output) = mapped_output {
|
||||
let geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
(
|
||||
geo.to_f64(),
|
||||
true,
|
||||
1. / output.current_scale().fractional_scale(),
|
||||
output.current_transform(),
|
||||
)
|
||||
} else {
|
||||
let geo = self.global_bounding_rectangle()?.to_f64();
|
||||
|
||||
// FIXME: this 1 px size should ideally somehow be computed for the rightmost output
|
||||
// corresponding to the position on the right when clamping.
|
||||
let output = self.niri.global_space.outputs().next().unwrap();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
|
||||
// Do not keep ratio for the unified mode as this is what OpenTabletDriver expects.
|
||||
(geo, false, 1. / scale, Transform::Normal)
|
||||
};
|
||||
|
||||
let mut pos = {
|
||||
let size = transform.invert().transform_size(target_geo.size);
|
||||
transform.transform_point_in(event.position_transformed(size), &size.to_f64())
|
||||
transform.transform_point_in(event.position_transformed(size.to_i32_round()), &size)
|
||||
};
|
||||
|
||||
if keep_ratio {
|
||||
pos.x /= target_geo.size.w as f64;
|
||||
pos.y /= target_geo.size.h as f64;
|
||||
pos.x /= target_geo.size.w;
|
||||
pos.y /= target_geo.size.h;
|
||||
|
||||
let device = event.device();
|
||||
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
|
||||
if let Some(data) = self.niri.tablets.get(device) {
|
||||
// This code does the same thing as mutter with "keep aspect ratio" enabled.
|
||||
let size = transform.invert().transform_size(target_geo.size);
|
||||
let output_aspect_ratio = size.w as f64 / size.h as f64;
|
||||
let output_aspect_ratio = size.w / size.h;
|
||||
let ratio = data.aspect_ratio / output_aspect_ratio;
|
||||
|
||||
if ratio > 1. {
|
||||
@@ -340,13 +368,13 @@ impl State {
|
||||
}
|
||||
};
|
||||
|
||||
pos.x *= target_geo.size.w as f64;
|
||||
pos.y *= target_geo.size.h as f64;
|
||||
pos.x *= target_geo.size.w;
|
||||
pos.y *= target_geo.size.h;
|
||||
}
|
||||
|
||||
pos.x = pos.x.clamp(0.0, target_geo.size.w as f64 - px);
|
||||
pos.y = pos.y.clamp(0.0, target_geo.size.h as f64 - px);
|
||||
Some(pos + target_geo.loc.to_f64())
|
||||
pos.x = pos.x.clamp(0.0, target_geo.size.w - px);
|
||||
pos.y = pos.y.clamp(0.0, target_geo.size.h - px);
|
||||
Some(pos + target_geo.loc)
|
||||
}
|
||||
|
||||
fn is_inhibiting_shortcuts(&self) -> bool {
|
||||
@@ -361,36 +389,11 @@ impl State {
|
||||
.is_some_and(KeyboardShortcutsInhibitor::is_active)
|
||||
}
|
||||
|
||||
fn on_keyboard<I: InputBackend + 'static>(
|
||||
fn on_keyboard<I: InputBackend>(
|
||||
&mut self,
|
||||
event: I::KeyboardKeyEvent,
|
||||
consumed_by_a11y: &mut bool,
|
||||
) where
|
||||
I::Device: 'static,
|
||||
{
|
||||
// Reset the keymap when handling a physical keyboard after a virtual one.
|
||||
if self.niri.reset_keymap {
|
||||
let device = event.device();
|
||||
let is_virtual_keyboard = (&device as &dyn Any)
|
||||
.downcast_ref::<VirtualKeyboard>()
|
||||
.is_some();
|
||||
if !is_virtual_keyboard {
|
||||
self.niri.reset_keymap = false;
|
||||
|
||||
let config = self.niri.config.borrow();
|
||||
let xkb_config = config.input.keyboard.xkb.clone();
|
||||
std::mem::drop(config);
|
||||
|
||||
if xkb_config != Xkb::default() {
|
||||
self.set_xkb_config(xkb_config.to_xkb_config());
|
||||
} else {
|
||||
// Use locale1 settings if xkb config is unset.
|
||||
let xkb = self.niri.xkb_from_locale1.clone().unwrap_or_default();
|
||||
self.set_xkb_config(xkb.to_xkb_config());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
) {
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
@@ -514,19 +517,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if pressed
|
||||
&& raw == Some(Keysym::Escape)
|
||||
&& (this.niri.pick_window.is_some() || this.niri.pick_color.is_some())
|
||||
{
|
||||
// We window picking state so the pick window grab must be active.
|
||||
// Unsetting it cancels window picking.
|
||||
this.niri
|
||||
.seat
|
||||
.get_pointer()
|
||||
.unwrap()
|
||||
.unset_grab(this, serial, time);
|
||||
this.niri.suppressed_keys.insert(key_code);
|
||||
return FilterResult::Intercept(None);
|
||||
if pressed && raw == Some(Keysym::Escape) {
|
||||
// Cancel certain grabs on Escape.
|
||||
let pointer = this.niri.seat.get_pointer().unwrap();
|
||||
if pointer
|
||||
.with_grab(|_, grab| Self::grab_can_be_cancelled_with_esc(grab))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
pointer.unset_grab(this, serial, time);
|
||||
this.niri.suppressed_keys.insert(key_code);
|
||||
return FilterResult::Intercept(None);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Keysym::space) = raw {
|
||||
@@ -2318,9 +2319,9 @@ impl State {
|
||||
}
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::LoadConfigFile => {
|
||||
Action::LoadConfigFile(path) => {
|
||||
if let Some(watcher) = &self.niri.config_file_watcher {
|
||||
watcher.load_config();
|
||||
watcher.load_config(path);
|
||||
}
|
||||
}
|
||||
Action::MruConfirm => {
|
||||
@@ -2545,16 +2546,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output() {
|
||||
let geom = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let mut point = (new_pos - geom.loc.to_f64())
|
||||
let point = (new_pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, None);
|
||||
}
|
||||
|
||||
@@ -2682,16 +2677,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output() {
|
||||
let geom = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, None);
|
||||
}
|
||||
|
||||
@@ -2776,10 +2765,11 @@ impl State {
|
||||
return;
|
||||
}
|
||||
|
||||
if ButtonState::Pressed == button_state {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
let mod_down = modifiers.contains(mod_key.to_modifiers());
|
||||
|
||||
if ButtonState::Pressed == button_state {
|
||||
let mut is_mru_open = false;
|
||||
if let Some(mru_output) = self.niri.window_mru_ui.output() {
|
||||
is_mru_open = true;
|
||||
@@ -2816,6 +2806,9 @@ impl State {
|
||||
let bindings =
|
||||
make_binds_iter(&config, &mut self.niri.window_mru_ui, modifiers);
|
||||
find_configured_bind(bindings, mod_key, trigger, mods)
|
||||
})
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open() || allowed_during_screenshot(&bind.action)
|
||||
}) {
|
||||
self.niri.suppressed_buttons.insert(button_code);
|
||||
self.handle_bind(bind.clone());
|
||||
@@ -2857,44 +2850,41 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if mod_down {
|
||||
let output_ws = if is_overview_open {
|
||||
self.niri.workspace_under_cursor(true)
|
||||
} else {
|
||||
// We don't want to accidentally "catch" the wrong workspace during
|
||||
// animations.
|
||||
self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
})
|
||||
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() && mod_down {
|
||||
let output_ws = if is_overview_open {
|
||||
self.niri.workspace_under_cursor(true)
|
||||
} else {
|
||||
// We don't want to accidentally "catch" the wrong workspace during
|
||||
// animations.
|
||||
self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
})
|
||||
};
|
||||
|
||||
if let Some((output, ws)) = output_ws {
|
||||
let ws_id = ws.id();
|
||||
|
||||
self.niri.layout.focus_output(&output);
|
||||
|
||||
let location = pointer.current_location();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
|
||||
|
||||
if let Some((output, ws)) = output_ws {
|
||||
let ws_id = ws.id();
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
self.niri.layout.focus_output(&output);
|
||||
|
||||
let location = pointer.current_location();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
// Don't activate the window under the cursor to avoid unnecessary
|
||||
// scrolling when e.g. Mod+MMB clicking on a partially off-screen window.
|
||||
return;
|
||||
}
|
||||
// Don't activate the window under the cursor to avoid unnecessary
|
||||
// scrolling when e.g. Mod+MMB clicking on a partially off-screen window.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2903,7 +2893,6 @@ impl State {
|
||||
|
||||
// Check if we need to start an interactive move.
|
||||
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if is_overview_open || mod_down {
|
||||
let location = pointer.current_location();
|
||||
|
||||
@@ -2937,72 +2926,69 @@ impl State {
|
||||
}
|
||||
}
|
||||
// Check if we need to start an interactive resize.
|
||||
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if mod_down {
|
||||
let location = pointer.current_location();
|
||||
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
|
||||
let edges = self
|
||||
else if button == Some(MouseButton::Right) && !pointer.is_grabbed() && mod_down {
|
||||
let location = pointer.current_location();
|
||||
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
|
||||
let edges = self
|
||||
.niri
|
||||
.layout
|
||||
.resize_edges_under(output, pos_within_output)
|
||||
.unwrap_or(ResizeEdge::empty());
|
||||
|
||||
if !edges.is_empty() {
|
||||
// See if we got a double resize-click gesture.
|
||||
// FIXME: deduplicate with resize_request in xdg-shell somehow.
|
||||
let time = get_monotonic_time();
|
||||
let last_cell = mapped.last_interactive_resize_start();
|
||||
let mut last = last_cell.get();
|
||||
last_cell.set(Some((time, edges)));
|
||||
|
||||
// Floating windows don't have either of the double-resize-click
|
||||
// gestures, so just allow it to resize.
|
||||
if mapped.is_floating() {
|
||||
last = None;
|
||||
last_cell.set(None);
|
||||
}
|
||||
|
||||
if let Some((last_time, last_edges)) = last {
|
||||
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
|
||||
// Allow quick resize after a triple click.
|
||||
last_cell.set(None);
|
||||
|
||||
let intersection = edges.intersection(last_edges);
|
||||
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
|
||||
// FIXME: don't activate once we can pass specific windows
|
||||
// to actions.
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.toggle_full_width();
|
||||
}
|
||||
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.reset_window_height(Some(&window));
|
||||
}
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
if self
|
||||
.niri
|
||||
.layout
|
||||
.resize_edges_under(output, pos_within_output)
|
||||
.unwrap_or(ResizeEdge::empty());
|
||||
|
||||
if !edges.is_empty() {
|
||||
// See if we got a double resize-click gesture.
|
||||
// FIXME: deduplicate with resize_request in xdg-shell somehow.
|
||||
let time = get_monotonic_time();
|
||||
let last_cell = mapped.last_interactive_resize_start();
|
||||
let mut last = last_cell.get();
|
||||
last_cell.set(Some((time, edges)));
|
||||
|
||||
// Floating windows don't have either of the double-resize-click
|
||||
// gestures, so just allow it to resize.
|
||||
if mapped.is_floating() {
|
||||
last = None;
|
||||
last_cell.set(None);
|
||||
}
|
||||
|
||||
if let Some((last_time, last_edges)) = last {
|
||||
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
|
||||
// Allow quick resize after a triple click.
|
||||
last_cell.set(None);
|
||||
|
||||
let intersection = edges.intersection(last_edges);
|
||||
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
|
||||
// FIXME: don't activate once we can pass specific windows
|
||||
// to actions.
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.toggle_full_width();
|
||||
}
|
||||
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.reset_window_height(Some(&window));
|
||||
}
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
if self
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_begin(window.clone(), edges)
|
||||
{
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = ResizeGrab::new(start_data, window.clone());
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri.cursor_manager.set_cursor_image(
|
||||
CursorImageStatus::Named(edges.cursor_icon()),
|
||||
);
|
||||
}
|
||||
.interactive_resize_begin(window.clone(), edges)
|
||||
{
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = ResizeGrab::new(start_data, window.clone());
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(edges.cursor_icon()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3042,20 +3028,25 @@ impl State {
|
||||
if button == Some(MouseButton::Left) && self.niri.screenshot_ui.is_open() {
|
||||
if button_state == ButtonState::Pressed {
|
||||
let pos = pointer.current_location();
|
||||
if let Some((output, _)) = self.niri.output_under(pos) {
|
||||
let output = output.clone();
|
||||
|
||||
// If we'll be moving the existing selection, use the selection output.
|
||||
let output = if mod_down {
|
||||
self.niri.screenshot_ui.selection_output()
|
||||
} else {
|
||||
self.niri.output_under(pos).map(|(out, _)| out)
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = min(size.w - 1, point.x);
|
||||
point.y = min(size.h - 1, point.y);
|
||||
|
||||
if self.niri.screenshot_ui.pointer_down(output, point, None) {
|
||||
if self
|
||||
.niri
|
||||
.screenshot_ui
|
||||
.pointer_down(output, point, None, mod_down)
|
||||
{
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
@@ -3171,13 +3162,21 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::WheelScrollLeft,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_right = find_configured_bind(
|
||||
bindings,
|
||||
mod_key,
|
||||
Trigger::WheelScrollRight,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
(bind_left, bind_right)
|
||||
};
|
||||
|
||||
@@ -3258,9 +3257,17 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::WheelScrollUp,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_down =
|
||||
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods);
|
||||
find_configured_bind(bindings, mod_key, Trigger::WheelScrollDown, mods)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
(bind_up, bind_down)
|
||||
};
|
||||
|
||||
@@ -3286,8 +3293,8 @@ impl State {
|
||||
let horizontal_amount = event.amount(Axis::Horizontal);
|
||||
let vertical_amount = event.amount(Axis::Vertical);
|
||||
|
||||
// Handle touchpad scroll bindings.
|
||||
if source == AxisSource::Finger {
|
||||
// Handle touchpad and continuous scroll bindings.
|
||||
if source == AxisSource::Finger || source == AxisSource::Continuous {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
|
||||
@@ -3403,9 +3410,17 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::TouchpadScrollLeft,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_right =
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollRight, mods);
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollRight, mods)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
drop(config);
|
||||
|
||||
if let Some(right) = bind_right {
|
||||
@@ -3433,9 +3448,17 @@ impl State {
|
||||
mod_key,
|
||||
Trigger::TouchpadScrollUp,
|
||||
mods,
|
||||
);
|
||||
)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
let bind_down =
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollDown, mods);
|
||||
find_configured_bind(bindings, mod_key, Trigger::TouchpadScrollDown, mods)
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
drop(config);
|
||||
|
||||
if let Some(down) = bind_down {
|
||||
@@ -3539,16 +3562,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output() {
|
||||
let geom = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, None);
|
||||
}
|
||||
|
||||
@@ -3621,19 +3638,29 @@ impl State {
|
||||
let under = self.niri.contents_under(pos);
|
||||
|
||||
if self.niri.screenshot_ui.is_open() {
|
||||
if let Some(output) = under.output.clone() {
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
let mod_down = modifiers.contains(mod_key.to_modifiers());
|
||||
|
||||
// If we'll be moving the existing selection, use the selection output.
|
||||
let output = if mod_down {
|
||||
self.niri.screenshot_ui.selection_output()
|
||||
} else {
|
||||
under.output.as_ref()
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = min(size.w - 1, point.x);
|
||||
point.y = min(size.h - 1, point.y);
|
||||
|
||||
if self.niri.screenshot_ui.pointer_down(output, point, None) {
|
||||
if self
|
||||
.niri
|
||||
.screenshot_ui
|
||||
.pointer_down(output, point, None, mod_down)
|
||||
{
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
@@ -3751,11 +3778,53 @@ impl State {
|
||||
}
|
||||
|
||||
fn on_tablet_tool_button<I: InputBackend>(&mut self, event: I::TabletToolButtonEvent) {
|
||||
const BTN_STYLUS3: u32 = 0x149;
|
||||
const BTN_STYLUS: u32 = 0x14b;
|
||||
const BTN_STYLUS2: u32 = 0x14c;
|
||||
|
||||
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
|
||||
|
||||
if let Some(tool) = tool {
|
||||
let button = event.button();
|
||||
|
||||
if self.niri.suppressed_buttons.remove(&button) {
|
||||
return;
|
||||
}
|
||||
|
||||
let trigger = match button {
|
||||
BTN_STYLUS => Some(Trigger::TabletStylusButton1),
|
||||
BTN_STYLUS2 => Some(Trigger::TabletStylusButton2),
|
||||
BTN_STYLUS3 => Some(Trigger::TabletStylusButton3),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(trigger) = trigger {
|
||||
if event.button_state() == ButtonState::Pressed {
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
|
||||
if self.niri.mods_with_tablet_stylus_binds.contains(&modifiers) {
|
||||
let bind = {
|
||||
let config = self.niri.config.borrow();
|
||||
let bindings = config.binds.0.iter();
|
||||
find_configured_bind(bindings, mod_key, trigger, mods)
|
||||
}
|
||||
.filter(|bind| {
|
||||
!self.niri.screenshot_ui.is_open()
|
||||
|| allowed_during_screenshot(&bind.action)
|
||||
});
|
||||
if let Some(bind) = bind {
|
||||
self.niri.suppressed_buttons.insert(button);
|
||||
self.handle_bind(bind.clone());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tool.button(
|
||||
event.button(),
|
||||
button,
|
||||
event.button_state(),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
event.time_msec(),
|
||||
@@ -4060,6 +4129,7 @@ impl State {
|
||||
fallback_output: Option<&Output>,
|
||||
) -> Option<Point<f64, Logical>> {
|
||||
let output = evt.device().output(self);
|
||||
let output = output.filter(|output| self.niri.output_exists(output));
|
||||
let output = output.as_ref().or(fallback_output)?;
|
||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let transform = output.current_transform();
|
||||
@@ -4094,24 +4164,28 @@ impl State {
|
||||
let under = self.niri.contents_under(pos);
|
||||
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let mods = modifiers_from_state(mods);
|
||||
let mod_down = mods.contains(mod_key.to_modifiers());
|
||||
|
||||
if self.niri.screenshot_ui.is_open() {
|
||||
if let Some(output) = under.output.clone() {
|
||||
// If we'll be moving the existing selection, use the selection output.
|
||||
let output = if mod_down {
|
||||
self.niri.screenshot_ui.selection_output()
|
||||
} else {
|
||||
under.output.as_ref()
|
||||
};
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = min(size.w - 1, point.x);
|
||||
point.y = min(size.h - 1, point.y);
|
||||
|
||||
if self
|
||||
.niri
|
||||
.screenshot_ui
|
||||
.pointer_down(output, point, Some(slot))
|
||||
.pointer_down(output, point, Some(slot), mod_down)
|
||||
{
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
@@ -4130,10 +4204,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
} else if !handle.is_grabbed() {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let mods = modifiers_from_state(mods);
|
||||
let mod_down = mods.contains(mod_key.to_modifiers());
|
||||
|
||||
if self.niri.layout.is_overview_open()
|
||||
&& !mod_down
|
||||
&& under.layer.is_none()
|
||||
@@ -4246,16 +4316,10 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.screenshot_ui.selection_output().cloned() {
|
||||
let geom = self.niri.global_space.output_geometry(&output).unwrap();
|
||||
let mut point = (pos - geom.loc.to_f64())
|
||||
let point = (pos - geom.loc.to_f64())
|
||||
.to_physical(output.current_scale().fractional_scale())
|
||||
.to_i32_round::<i32>();
|
||||
|
||||
let size = output.current_mode().unwrap().size;
|
||||
let transform = output.current_transform();
|
||||
let size = transform.transform_size(size);
|
||||
point.x = point.x.clamp(0, size.w - 1);
|
||||
point.y = point.y.clamp(0, size.h - 1);
|
||||
|
||||
self.niri.screenshot_ui.pointer_motion(point, Some(slot));
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
@@ -4322,6 +4386,12 @@ impl State {
|
||||
// Null-source DnD: weston-dnd --self-only
|
||||
|| grab.is::<DnDGrab<Self, WlSurface, WlSurface>>()
|
||||
}
|
||||
|
||||
fn grab_can_be_cancelled_with_esc(grab: &(dyn PointerGrab<State> + 'static)) -> bool {
|
||||
let grab = grab.as_any();
|
||||
|
||||
grab.is::<PickWindowGrab>() || grab.is::<PickColorGrab>() || Self::is_dnd_grab(grab)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the key should be intercepted and mark intercepted
|
||||
@@ -4625,6 +4695,9 @@ fn allowed_during_screenshot(action: &Action) -> bool {
|
||||
| Action::Suspend
|
||||
| Action::PowerOffMonitors
|
||||
| Action::PowerOnMonitors
|
||||
// Intended for binds such as volume up/down, lock the screen, etc.
|
||||
| Action::Spawn(_)
|
||||
| Action::SpawnSh(_)
|
||||
// The screenshot UI can handle these.
|
||||
| Action::MoveColumnLeft
|
||||
| Action::MoveColumnLeftOrToMonitorLeft
|
||||
@@ -4704,7 +4777,11 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
|
||||
let _ = device.config_tap_set_enabled(c.tap);
|
||||
let _ = device.config_dwt_set_enabled(c.dwt);
|
||||
let _ = device.config_dwtp_set_enabled(c.dwtp);
|
||||
let _ = device.config_tap_set_drag_lock_enabled(c.drag_lock);
|
||||
let _ = device.config_tap_set_drag_lock_enabled(if c.drag_lock {
|
||||
input::DragLockState::EnabledTimeout
|
||||
} else {
|
||||
input::DragLockState::Disabled
|
||||
});
|
||||
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||
let _ = device.config_accel_set_speed(c.accel_speed.0);
|
||||
let _ = device.config_left_handed_set(c.left_handed);
|
||||
@@ -5034,6 +5111,18 @@ pub fn mods_with_finger_scroll_binds(mod_key: ModKey, binds: &Binds) -> HashSet<
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mods_with_tablet_stylus_binds(mod_key: ModKey, binds: &Binds) -> HashSet<Modifiers> {
|
||||
mods_with_binds(
|
||||
mod_key,
|
||||
binds,
|
||||
&[
|
||||
Trigger::TabletStylusButton1,
|
||||
Trigger::TabletStylusButton2,
|
||||
Trigger::TabletStylusButton3,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn grab_allows_hot_corner(grab: &(dyn PointerGrab<State> + 'static)) -> bool {
|
||||
let grab = grab.as_any();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use smithay::input::SeatHandler;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::render_helpers::{render_and_download, RenderTarget};
|
||||
use crate::render_helpers::{render_and_download, RenderCtx, RenderTarget};
|
||||
|
||||
pub struct PickColorGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
@@ -49,13 +49,13 @@ impl PickColorGrab {
|
||||
let pos = pos_within_output.to_physical_precise_floor(scale);
|
||||
let size = Size::<i32, Physical>::from((1, 1));
|
||||
|
||||
let elements = data.niri.render(
|
||||
let ctx = RenderCtx {
|
||||
renderer,
|
||||
&output,
|
||||
false,
|
||||
// This is an interactive operation so we can render without blocking out.
|
||||
RenderTarget::Output,
|
||||
);
|
||||
target: RenderTarget::Output,
|
||||
xray: None,
|
||||
};
|
||||
let elements = data.niri.render_to_vec(ctx, &output, false);
|
||||
|
||||
let mapping = match render_and_download(
|
||||
renderer,
|
||||
|
||||
@@ -193,7 +193,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
|
||||
windows.sort_unstable_by_key(|a| a.id);
|
||||
|
||||
for window in windows {
|
||||
print_window(&window);
|
||||
@@ -220,7 +220,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
|
||||
let print = |surface: &niri_ipc::LayerSurface| {
|
||||
println!(" Surface:");
|
||||
println!(" Namespace: \"{}\"", &surface.namespace);
|
||||
println!(" Namespace: \"{}\"", surface.namespace);
|
||||
|
||||
let interactivity = match surface.keyboard_interactivity {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
|
||||
@@ -568,6 +568,7 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
max_bpc,
|
||||
} = output;
|
||||
|
||||
let serial = serial.as_deref().unwrap_or("Unknown");
|
||||
@@ -651,6 +652,10 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
println!(" Transform: {transform}");
|
||||
}
|
||||
|
||||
if let Some(max_bpc) = max_bpc {
|
||||
println!(" Max bits per channel: {max_bpc}");
|
||||
}
|
||||
|
||||
println!(" Available modes:");
|
||||
for (idx, mode) in modes.into_iter().enumerate() {
|
||||
let Mode {
|
||||
|
||||