mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
183 Commits
v0.1.0-alpha.1
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| dd967554d1 | |||
| 6d7c220137 | |||
| d77aac1afa | |||
| 837a0a20fb | |||
| ecdf756b55 | |||
| 73f3c160b2 | |||
| 5f99eb13ab | |||
| 20326b093c | |||
| 467d92a4b4 | |||
| 15bb69c0b9 | |||
| adfbfdffb3 | |||
| 087ed260c5 | |||
| f5642ab733 | |||
| ab9706cb30 | |||
| 05f2a3709b | |||
| 743173ef64 | |||
| cbbb7a26fc | |||
| 18566e3366 | |||
| df48337d83 | |||
| f5e9b40140 | |||
| 5cacd03e85 | |||
| 6945ccde18 | |||
| e86e9c6c9a | |||
| dc47de178f | |||
| 65e864965e | |||
| 55ad36addc | |||
| 26c8cbb961 | |||
| 031133c052 | |||
| a6f821d3fa | |||
| 475b3df2b5 | |||
| 1541835f00 | |||
| 4b9cb2f0d3 | |||
| 3461c66d2c | |||
| 011c91c98a | |||
| edafa139f6 | |||
| fa9b3ed106 | |||
| cc62a403c0 | |||
| 0f85c79548 | |||
| 6beef26662 | |||
| 616055e205 | |||
| 40c85da102 | |||
| 768b326028 | |||
| f068157f55 | |||
| 6703d5ce72 | |||
| 12590f689a | |||
| 4656332d07 | |||
| 954f711bf3 | |||
| c09c964420 | |||
| 1f9abaaa58 | |||
| eb4946c3d8 | |||
| 5f440f7be3 | |||
| 6644cc16ff | |||
| 9e667efc4c | |||
| 8a7e4bc3cd | |||
| 69907f123d | |||
| 6ca3b6ddb5 | |||
| fc5a080ca5 | |||
| 83719a49b7 | |||
| da4967d43c | |||
| d958a9679c | |||
| e4643c6dbe | |||
| 59763fd0da | |||
| 533659eef8 | |||
| 81443d8e16 | |||
| fb38ae26c9 | |||
| cc4acdf24a | |||
| 2506d43bb9 | |||
| d899bc4712 | |||
| 14552d856c | |||
| 632a00fcca | |||
| 80652a0765 | |||
| a52bf92ae1 | |||
| 952ff02982 | |||
| e1adabed2d | |||
| b5c4f9ed2a | |||
| d39f1897c7 | |||
| e46b614c2b | |||
| 78aa08b100 | |||
| d8626fcab0 | |||
| f4e04ac910 | |||
| 236abd9d9d | |||
| b2df3e104f | |||
| ec2d339a86 | |||
| 629a2ccb47 | |||
| fb93038bd8 | |||
| 71fef2ad2e | |||
| c6841f19e9 | |||
| e1971c4af5 | |||
| 07b1d0e98d | |||
| ffe25f5cc4 | |||
| 43e2cf14d2 | |||
| 2c59131f7f | |||
| 64c41fa2c8 | |||
| 4e0aa39113 | |||
| dcb80efc88 | |||
| 3d5de921cd | |||
| 8703feedee | |||
| a27d22571d | |||
| d10af92aea | |||
| 0bc83eda71 | |||
| 6fce5662e7 | |||
| 1c7c5b3f28 | |||
| b9d7812f1f | |||
| 655b9808b9 | |||
| 5cd31e5730 | |||
| de3fc2def0 | |||
| fd1d4b07fd | |||
| 8b5acd5e6e | |||
| 31bb9096e2 | |||
| dae93ee159 | |||
| 57a7347620 | |||
| 628891db2c | |||
| be6e25f5fb | |||
| e005a795e7 | |||
| 655fe413fb | |||
| ac6ff7ff41 | |||
| 84befb4e91 | |||
| d39f7bebf3 | |||
| 0dd9a42087 | |||
| 658941f2c3 | |||
| 6ccc4147ae | |||
| 46d5f5ec4d | |||
| c64e96d0d8 | |||
| 44d3a5b9a2 | |||
| 5d95de97a0 | |||
| 56174b2c34 | |||
| 310aa2b464 | |||
| d6c553091f | |||
| 097c415036 | |||
| 2d16c04869 | |||
| 249f2b7a21 | |||
| f3e5e13c45 | |||
| b13892ca63 | |||
| 777ad4ee5c | |||
| c21805bf70 | |||
| bfc2418267 | |||
| 77b4715e0b | |||
| c048abc8b5 | |||
| 4dd7578fe7 | |||
| ea72e4dae8 | |||
| 0c671ee493 | |||
| 324c1efd04 | |||
| 99e75b95b7 | |||
| 489a225fae | |||
| 85cb4b42f6 | |||
| be2e551a89 | |||
| ed3080d908 | |||
| 461ce5f363 | |||
| 624c799ebf | |||
| 57f267454f | |||
| 86c4c1368e | |||
| 17c23dc50f | |||
| 5b1de86d33 | |||
| 58162ce685 | |||
| 9ac925ea0c | |||
| 0f83eacb42 | |||
| e259061cbc | |||
| 206493bb35 | |||
| c29a049245 | |||
| d6b62ad09d | |||
| d155f5cd6c | |||
| 74ff4f1903 | |||
| 8c3107af7b | |||
| 8bcd18ace2 | |||
| 4fefab7d6b | |||
| 675932c05b | |||
| 475d6e4be1 | |||
| d9e27988a7 | |||
| 1be860c527 | |||
| b3e0a6c543 | |||
| 23a5bd3670 | |||
| d397375d57 | |||
| cb3ba5105d | |||
| 243519598e | |||
| 0b5f232bc2 | |||
| 9b3478a3d7 | |||
| cb1e5d6c19 | |||
| 11ae17b220 | |||
| 40b633be5c | |||
| 0e29e7f6ff | |||
| 626c720b7a | |||
| 3f76b71115 | |||
| 1599a01f3b |
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report a bug or a crash
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||||
|
|
||||||
|
### System Information
|
||||||
|
|
||||||
|
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
||||||
|
* niri version:
|
||||||
|
|
||||||
|
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
|
||||||
|
* GPU:
|
||||||
|
|
||||||
|
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
|
||||||
|
* CPU:
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
contact_links:
|
||||||
|
- name: Feature request
|
||||||
|
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
||||||
|
about: Ideas for new features and functionality (start a Discussion)
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
sudo apt-get install -y software-properties-common
|
sudo apt-get install -y software-properties-common
|
||||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
|
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
sudo apt-get install -y software-properties-common
|
sudo apt-get install -y software-properties-common
|
||||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
|
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
@@ -107,3 +107,20 @@ jobs:
|
|||||||
- name: Run rustfmt
|
- name: Run rustfmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
fedora:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container: fedora:39
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo dnf update -y
|
||||||
|
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo build
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/result
|
||||||
|
|||||||
Generated
+669
-376
File diff suppressed because it is too large
Load Diff
+66
-38
@@ -1,48 +1,75 @@
|
|||||||
[package]
|
[workspace.package]
|
||||||
name = "niri"
|
version = "0.1.0"
|
||||||
version = "0.1.0-alpha.1"
|
|
||||||
description = "A scrollable-tiling Wayland compositor"
|
description = "A scrollable-tiling Wayland compositor"
|
||||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
readme = "README.md"
|
|
||||||
repository = "https://github.com/YaLTeR/niri"
|
repository = "https://github.com/YaLTeR/niri"
|
||||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
|
||||||
|
|
||||||
[dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = { version = "1.0.75" }
|
bitflags = "2.4.2"
|
||||||
arrayvec = "0.7.4"
|
|
||||||
async-channel = { version = "2.1.0", optional = true }
|
|
||||||
async-io = { version = "1.13.0", optional = true }
|
|
||||||
bitflags = "2.4.1"
|
|
||||||
clap = { version = "4.4.8", features = ["derive"] }
|
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
git-version = "0.3.8"
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
keyframe = { version = "1.1.1", default-features = false }
|
|
||||||
knuffel = "3.2.0"
|
|
||||||
libc = "0.2.150"
|
|
||||||
logind-zbus = { version = "3.1.2", optional = true }
|
|
||||||
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
|
||||||
miette = "5.10.0"
|
|
||||||
notify-rust = { version = "4.10.0", optional = true }
|
|
||||||
pipewire = { version = "0.7.2", optional = true }
|
|
||||||
png = "0.17.10"
|
|
||||||
portable-atomic = { version = "1.5.1", default-features = false, features = ["float"] }
|
|
||||||
profiling = "1.0.11"
|
|
||||||
sd-notify = "0.4.1"
|
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
|
||||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
tracy-client = { version = "0.16.4", default-features = false }
|
tracy-client = { version = "0.16.5", default-features = false }
|
||||||
url = { version = "2.5.0", optional = true }
|
|
||||||
xcursor = "0.3.4"
|
|
||||||
zbus = { version = "3.14.1", optional = true }
|
|
||||||
|
|
||||||
[dependencies.smithay]
|
[workspace.dependencies.smithay]
|
||||||
git = "https://github.com/Smithay/smithay.git"
|
git = "https://github.com/Smithay/smithay.git"
|
||||||
# path = "../smithay"
|
# path = "../smithay"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
|
[workspace.dependencies.smithay-drm-extras]
|
||||||
|
git = "https://github.com/Smithay/smithay.git"
|
||||||
|
# path = "../smithay/smithay-drm-extras"
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "niri"
|
||||||
|
version.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { version = "1.0.79" }
|
||||||
|
arrayvec = "0.7.4"
|
||||||
|
async-channel = { version = "2.1.1", optional = true }
|
||||||
|
async-io = { version = "1.13.0", optional = true }
|
||||||
|
bitflags = "2.4.2"
|
||||||
|
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
|
||||||
|
clap = { version = "4.4.18", features = ["derive", "string"] }
|
||||||
|
directories = "5.0.1"
|
||||||
|
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||||
|
git-version = "0.3.9"
|
||||||
|
keyframe = { version = "1.1.1", default-features = false }
|
||||||
|
libc = "0.2.152"
|
||||||
|
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
|
logind-zbus = { version = "3.1.2", optional = true }
|
||||||
|
niri-config = { version = "0.1.0", path = "niri-config" }
|
||||||
|
niri-ipc = { version = "0.1.0", path = "niri-ipc" }
|
||||||
|
notify-rust = { version = "4.10.0", optional = true }
|
||||||
|
pangocairo = "0.18.0"
|
||||||
|
pipewire = { version = "0.7.2", optional = true }
|
||||||
|
png = "0.17.11"
|
||||||
|
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||||
|
profiling = "1.0.13"
|
||||||
|
sd-notify = "0.4.1"
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json = "1.0.111"
|
||||||
|
smithay-drm-extras.workspace = true
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
tracing.workspace = true
|
||||||
|
tracy-client.workspace = true
|
||||||
|
url = { version = "2.5.0", optional = true }
|
||||||
|
xcursor = "0.3.5"
|
||||||
|
zbus = { version = "3.14.1", optional = true }
|
||||||
|
|
||||||
|
[dependencies.smithay]
|
||||||
|
workspace = true
|
||||||
features = [
|
features = [
|
||||||
"backend_drm",
|
"backend_drm",
|
||||||
"backend_egl",
|
"backend_egl",
|
||||||
@@ -52,17 +79,13 @@ features = [
|
|||||||
"backend_udev",
|
"backend_udev",
|
||||||
"backend_winit",
|
"backend_winit",
|
||||||
"desktop",
|
"desktop",
|
||||||
"libinput_1_19",
|
|
||||||
"renderer_gl",
|
"renderer_gl",
|
||||||
|
"renderer_pixman",
|
||||||
"renderer_multi",
|
"renderer_multi",
|
||||||
"use_system_lib",
|
"use_system_lib",
|
||||||
"wayland_frontend",
|
"wayland_frontend",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies.smithay-drm-extras]
|
|
||||||
git = "https://github.com/Smithay/smithay.git"
|
|
||||||
# path = "../smithay/smithay-drm-extras"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1.4.0"
|
proptest = "1.4.0"
|
||||||
proptest-derive = "0.4.0"
|
proptest-derive = "0.4.0"
|
||||||
@@ -81,7 +104,12 @@ debug = "line-tables-only"
|
|||||||
overflow-checks = true
|
overflow-checks = true
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|
||||||
|
[profile.release.package.niri-config]
|
||||||
|
# knuffel with chomsky generates a metric ton of debuginfo.
|
||||||
|
debug = false
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
|
version = "0.1.0"
|
||||||
assets = [
|
assets = [
|
||||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||||
|
|||||||
@@ -1,38 +1,65 @@
|
|||||||
# niri
|
<h1 align="center">niri</h1>
|
||||||
|
<p align="center">A scrollable-tiling Wayland compositor.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=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>
|
||||||
|
</p>
|
||||||
|
|
||||||
A scrollable-tiling Wayland compositor.
|

|
||||||
|
|
||||||

|
## About
|
||||||
|
|
||||||
|
Windows are arranged in columns on an infinite strip going to the right.
|
||||||
|
Opening a new window never causes existing windows to resize.
|
||||||
|
|
||||||
|
Every monitor has its own separate window strip.
|
||||||
|
Windows can never "overflow" onto an adjacent monitor.
|
||||||
|
|
||||||
|
Workspaces are dynamic and arranged vertically.
|
||||||
|
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
|
||||||
|
|
||||||
|
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
|
||||||
|
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Scrollable tiling
|
||||||
|
- Dynamic workspaces like in GNOME
|
||||||
|
- Built-in screenshot UI
|
||||||
|
- Monitor screencasting through xdg-desktop-portal-gnome
|
||||||
|
- Touchpad gesture to switch workspaces
|
||||||
|
- Configurable layout: gaps, borders, struts, window sizes
|
||||||
|
- Live-reloading config
|
||||||
|
|
||||||
|
## Video Demo
|
||||||
|
|
||||||
|
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
A lot of the essential functionality is implemented, plus some goodies on top.
|
A lot of the essential functionality is implemented, plus some goodies on top.
|
||||||
Feel free to give niri a try.
|
Feel free to give niri a try.
|
||||||
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
|
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||||
|
|
||||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
Note that NVIDIA GPUs might have rendering issues.
|
||||||
|
|
||||||
## Idea
|
## Inspiration
|
||||||
|
|
||||||
Niri implements scrollable tiling, heavily inspired by [PaperWM].
|
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
|
||||||
Windows are arranged in columns on an infinite strip going to the right.
|
|
||||||
Every column takes up a full monitor worth of height, divided among its windows.
|
|
||||||
|
|
||||||
With multiple monitors, every monitor has its own separate window strip.
|
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
|
||||||
Windows can never "overflow" onto an adjacent monitor.
|
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
|
||||||
|
|
||||||
This is one of the reasons that prompted me to try writing my own compositor.
|
|
||||||
PaperWM is a solid implementation, but, being a GNOME Shell extension, it has to work around Shell's global window coordinate space to prevent windows from overflowing.
|
|
||||||
|
|
||||||
Niri also has dynamic workspaces which work similar to GNOME Shell.
|
|
||||||
Since windows go left-to-right horizontally, workspaces are arranged vertically.
|
|
||||||
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
|
|
||||||
|
|
||||||
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
|
|
||||||
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
||||||
|
>
|
||||||
|
> NixOS users, check out https://github.com/sodiboo/niri-flake
|
||||||
|
>
|
||||||
|
> For Arch users, there's an AUR package: https://aur.archlinux.org/packages/niri
|
||||||
|
|
||||||
First, install the dependencies for your distribution.
|
First, install the dependencies for your distribution.
|
||||||
|
|
||||||
- Ubuntu:
|
- Ubuntu:
|
||||||
@@ -41,16 +68,28 @@ First, install the dependencies for your distribution.
|
|||||||
sudo apt-get install -y software-properties-common
|
sudo apt-get install -y software-properties-common
|
||||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
|
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- Fedora:
|
- Fedora:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel clang
|
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, build niri with `cargo build --release`.
|
Next, get latest stable Rust: https://rustup.rs/
|
||||||
|
|
||||||
|
Then, build niri with `cargo build --release`.
|
||||||
|
|
||||||
|
### NixOS/Nix
|
||||||
|
|
||||||
|
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
|
||||||
|
|
||||||
|
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix run --impure github:guibou/nixGL -- ./results/bin/niri
|
||||||
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -84,10 +123,28 @@ Starting it from there will run niri as a desktop session.
|
|||||||
|
|
||||||
The niri session will autostart apps through the systemd xdg-autostart target.
|
The niri session will autostart apps through the systemd xdg-autostart target.
|
||||||
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
|
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
|
||||||
|
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
|
||||||
|
|
||||||
Niri also works with some parts of xdg-desktop-portal-gnome.
|
Niri also works with some parts of xdg-desktop-portal-gnome.
|
||||||
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
|
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
|
||||||
|
|
||||||
|
[This wiki page](https://github.com/YaLTeR/niri/wiki/Important-Software) explains how to run important software required for normal desktop use, including portals.
|
||||||
|
|
||||||
|
### Xwayland
|
||||||
|
|
||||||
|
See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri.
|
||||||
|
|
||||||
|
### IPC
|
||||||
|
|
||||||
|
You can communicate with the running niri instance over an IPC socket.
|
||||||
|
Check `niri msg --help` for available commands.
|
||||||
|
|
||||||
|
The `--json` flag prints the response in JSON, rather than formatted.
|
||||||
|
For example, `niri msg --json outputs`.
|
||||||
|
|
||||||
|
For programmatic access, check the [niri-ipc sub-crate](./niri-ipc/) which defines the types.
|
||||||
|
The communication over the IPC socket happens in JSON.
|
||||||
|
|
||||||
## Default Hotkeys
|
## Default Hotkeys
|
||||||
|
|
||||||
When running on a TTY, the Mod key is <kbd>Super</kbd>.
|
When running on a TTY, the Mod key is <kbd>Super</kbd>.
|
||||||
@@ -97,6 +154,7 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
|||||||
|
|
||||||
| Hotkey | Description |
|
| Hotkey | Description |
|
||||||
| ------ | ----------- |
|
| ------ | ----------- |
|
||||||
|
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
|
||||||
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
|
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
|
||||||
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
|
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
|
||||||
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
|
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
|
||||||
@@ -109,14 +167,16 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
|||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
|
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
|
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
|
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
|
||||||
|
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
|
||||||
|
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
|
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused window to the monitor to the side |
|
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused column to the monitor to the side |
|
||||||
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
|
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
|
||||||
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
|
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused window to the workspace below |
|
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused column to the workspace below |
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused window to the workspace above |
|
| <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>1</kbd>–<kbd>9</kbd> | Switch to a workspace by index |
|
| <kbd>Mod</kbd><kbd>1</kbd>–<kbd>9</kbd> | Switch to a workspace by index |
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd>–<kbd>9</kbd> | Move the focused window to a workspace by index |
|
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd>–<kbd>9</kbd> | Move the focused column to a workspace by index |
|
||||||
| <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>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>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> | Consume the window to the right into the focused column |
|
||||||
@@ -141,11 +201,15 @@ Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or
|
|||||||
If this fails, it will load [the default configuration file](resources/default-config.kdl).
|
If this fails, it will load [the default configuration file](resources/default-config.kdl).
|
||||||
Please use the default configuration file as the starting point for your custom configuration.
|
Please use the default configuration file as the starting point for your custom configuration.
|
||||||
|
|
||||||
Niri will live-reload many of the configuration settings, like key binds or gaps, as you change the config file.
|
Niri will live-reload most of the configuration settings, like key binds or gaps or output modes, as you change the config file.
|
||||||
Though, some settings are still missing live-reload support.
|
|
||||||
Notably, output modes and positions will only apply when the output is reconnected.
|
## Contact
|
||||||
|
|
||||||
|
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||||
|
|
||||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||||
[mako]: https://github.com/emersion/mako
|
[mako]: https://github.com/emersion/mako
|
||||||
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
||||||
|
[waybar]: https://github.com/Alexays/Waybar
|
||||||
|
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||||
|
|
||||||
|
|||||||
Generated
+138
@@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1702918879,
|
||||||
|
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701411808,
|
||||||
|
"narHash": "sha256-K8QDx8UgbvGdENuvPvcsCXcd8brd55OkRDFLBT7xUVY=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "3776d0e2a30184cc6a0ba20fb86dc6df5b41fccd",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"ref": "monthly",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701680307,
|
||||||
|
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix-filter": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701697642,
|
||||||
|
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "nix-filter",
|
||||||
|
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "nix-filter",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1702900294,
|
||||||
|
"narHash": "sha256-pt7sSoJYNw3n8YtXw0Z/Nnr6/PfY2YrjDvqboErXnRM=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "886c9aee6ca9324e127f9c2c4e6f68c2641c8256",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nix-filter": "nix-filter",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701372675,
|
||||||
|
"narHash": "sha256-MSHhnAoLjJuoPxzsTzBOzNhjhlCTHPs4nvkPAZVV1eY=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "c9d189d1375e59a6c9b4d62fdede94ade001f6ee",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# This flake file is community maintained
|
||||||
|
# Maintainers:
|
||||||
|
# Bill Sun (github/billksun)
|
||||||
|
{
|
||||||
|
description = "Niri: A scrollable-tiling Wayland compositor.";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
crane = {
|
||||||
|
url = "github:ipetkov/crane";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
nix-filter.url = "github:numtide/nix-filter";
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix/monthly";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
crane,
|
||||||
|
nix-filter,
|
||||||
|
flake-utils,
|
||||||
|
fenix,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
systems = ["aarch64-linux" "x86_64-linux"];
|
||||||
|
in
|
||||||
|
flake-utils.lib.eachSystem systems (
|
||||||
|
system: let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
toolchain = fenix.packages.${system}.complete.toolchain;
|
||||||
|
craneLib = crane.lib.${system}.overrideToolchain toolchain;
|
||||||
|
|
||||||
|
craneArgs = {
|
||||||
|
pname = "niri";
|
||||||
|
version = self.rev or "dirty";
|
||||||
|
|
||||||
|
src = nix-filter.lib.filter {
|
||||||
|
root = ./.;
|
||||||
|
include = [
|
||||||
|
./src
|
||||||
|
./niri-config
|
||||||
|
./niri-ipc
|
||||||
|
./Cargo.toml
|
||||||
|
./Cargo.lock
|
||||||
|
./resources
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
pkg-config
|
||||||
|
autoPatchelfHook
|
||||||
|
clang
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
wayland
|
||||||
|
systemd # For libudev
|
||||||
|
seatd # For libseat
|
||||||
|
libxkbcommon
|
||||||
|
libinput
|
||||||
|
mesa # For libgbm
|
||||||
|
fontconfig
|
||||||
|
stdenv.cc.cc.lib
|
||||||
|
pipewire
|
||||||
|
pango
|
||||||
|
];
|
||||||
|
|
||||||
|
runtimeDependencies = with pkgs; [
|
||||||
|
wayland
|
||||||
|
mesa
|
||||||
|
libglvnd # For libEGL
|
||||||
|
];
|
||||||
|
|
||||||
|
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
|
||||||
|
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
|
||||||
|
in {
|
||||||
|
formatter = pkgs.alejandra;
|
||||||
|
|
||||||
|
checks.niri = niri;
|
||||||
|
packages.default = niri;
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
|
||||||
|
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
|
||||||
|
packages = niri.runtimeDependencies;
|
||||||
|
|
||||||
|
# Force linking to libEGL, which is always dlopen()ed, and to
|
||||||
|
# libwayland-client, which is always dlopen()ed except by the
|
||||||
|
# obscure winit backend.
|
||||||
|
RUSTFLAGS = map (a: "-C link-arg=${a}") [
|
||||||
|
"-Wl,--push-state,--no-as-needed"
|
||||||
|
"-lEGL"
|
||||||
|
"-lwayland-client"
|
||||||
|
"-Wl,--pop-state"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "niri-config"
|
||||||
|
version.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitflags.workspace = true
|
||||||
|
knuffel = "3.2.0"
|
||||||
|
miette = "5.10.0"
|
||||||
|
smithay.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracy-client.workspace = true
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
use std::path::PathBuf;
|
#[macro_use]
|
||||||
|
extern crate tracing;
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use directories::ProjectDirs;
|
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
|
||||||
use miette::{miette, Context, IntoDiagnostic};
|
|
||||||
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
|
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
|
||||||
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
|
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
|
||||||
use smithay::input::keyboard::Keysym;
|
use smithay::input::keyboard::{Keysym, XkbConfig};
|
||||||
|
use smithay::reexports::input;
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, PartialEq)]
|
#[derive(knuffel::Decode, Debug, PartialEq)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -17,17 +20,11 @@ pub struct Config {
|
|||||||
#[knuffel(children(name = "spawn-at-startup"))]
|
#[knuffel(children(name = "spawn-at-startup"))]
|
||||||
pub spawn_at_startup: Vec<SpawnAtStartup>,
|
pub spawn_at_startup: Vec<SpawnAtStartup>,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
pub focus_ring: FocusRing,
|
pub layout: Layout,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
pub prefer_no_csd: bool,
|
pub prefer_no_csd: bool,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
pub cursor: Cursor,
|
pub cursor: Cursor,
|
||||||
#[knuffel(child, unwrap(children), default)]
|
|
||||||
pub preset_column_widths: Vec<PresetWidth>,
|
|
||||||
#[knuffel(child)]
|
|
||||||
pub default_column_width: Option<DefaultColumnWidth>,
|
|
||||||
#[knuffel(child, unwrap(argument), default = 16)]
|
|
||||||
pub gaps: u16,
|
|
||||||
#[knuffel(
|
#[knuffel(
|
||||||
child,
|
child,
|
||||||
unwrap(argument),
|
unwrap(argument),
|
||||||
@@ -37,6 +34,8 @@ pub struct Config {
|
|||||||
]
|
]
|
||||||
pub screenshot_path: Option<String>,
|
pub screenshot_path: Option<String>,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
|
pub hotkey_overlay: HotkeyOverlay,
|
||||||
|
#[knuffel(child, default)]
|
||||||
pub binds: Binds,
|
pub binds: Binds,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
pub debug: DebugConfig,
|
pub debug: DebugConfig,
|
||||||
@@ -50,7 +49,11 @@ pub struct Input {
|
|||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
pub touchpad: Touchpad,
|
pub touchpad: Touchpad,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
|
pub mouse: Mouse,
|
||||||
|
#[knuffel(child, default)]
|
||||||
pub tablet: Tablet,
|
pub tablet: Tablet,
|
||||||
|
#[knuffel(child)]
|
||||||
|
pub disable_power_key_handling: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
|
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
|
||||||
@@ -66,7 +69,7 @@ pub struct Keyboard {
|
|||||||
pub track_layout: TrackLayout,
|
pub track_layout: TrackLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
|
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq, Clone)]
|
||||||
pub struct Xkb {
|
pub struct Xkb {
|
||||||
#[knuffel(child, unwrap(argument), default)]
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
pub rules: String,
|
pub rules: String,
|
||||||
@@ -80,6 +83,30 @@ pub struct Xkb {
|
|||||||
pub options: Option<String>,
|
pub options: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Xkb {
|
||||||
|
pub fn to_xkb_config(&self) -> XkbConfig {
|
||||||
|
XkbConfig {
|
||||||
|
rules: &self.rules,
|
||||||
|
model: &self.model,
|
||||||
|
layout: self.layout.as_deref().unwrap_or("us"),
|
||||||
|
variant: &self.variant,
|
||||||
|
options: self.options.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum CenterFocusedColumn {
|
||||||
|
/// Focusing a column will not center the column.
|
||||||
|
#[default]
|
||||||
|
Never,
|
||||||
|
/// The focused column will always be centered.
|
||||||
|
Always,
|
||||||
|
/// Focusing a column will center it if it doesn't fit on the screen together with the
|
||||||
|
/// previously focused column.
|
||||||
|
OnOverflow,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)]
|
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum TrackLayout {
|
pub enum TrackLayout {
|
||||||
/// The layout change is global.
|
/// The layout change is global.
|
||||||
@@ -95,9 +122,55 @@ pub struct Touchpad {
|
|||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
pub tap: bool,
|
pub tap: bool,
|
||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
|
pub dwt: bool,
|
||||||
|
#[knuffel(child)]
|
||||||
pub natural_scroll: bool,
|
pub natural_scroll: bool,
|
||||||
#[knuffel(child, unwrap(argument), default)]
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
pub accel_speed: f64,
|
pub accel_speed: f64,
|
||||||
|
#[knuffel(child, unwrap(argument, str))]
|
||||||
|
pub accel_profile: Option<AccelProfile>,
|
||||||
|
#[knuffel(child, unwrap(argument, str))]
|
||||||
|
pub tap_button_map: Option<TapButtonMap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||||
|
pub struct Mouse {
|
||||||
|
#[knuffel(child)]
|
||||||
|
pub natural_scroll: bool,
|
||||||
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
|
pub accel_speed: f64,
|
||||||
|
#[knuffel(child, unwrap(argument, str))]
|
||||||
|
pub accel_profile: Option<AccelProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AccelProfile {
|
||||||
|
Adaptive,
|
||||||
|
Flat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AccelProfile> for input::AccelProfile {
|
||||||
|
fn from(value: AccelProfile) -> Self {
|
||||||
|
match value {
|
||||||
|
AccelProfile::Adaptive => Self::Adaptive,
|
||||||
|
AccelProfile::Flat => Self::Flat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TapButtonMap {
|
||||||
|
LeftRightMiddle,
|
||||||
|
LeftMiddleRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TapButtonMap> for input::TapButtonMap {
|
||||||
|
fn from(value: TapButtonMap) -> Self {
|
||||||
|
match value {
|
||||||
|
TapButtonMap::LeftRightMiddle => Self::LeftRightMiddle,
|
||||||
|
TapButtonMap::LeftMiddleRight => Self::LeftMiddleRight,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||||
@@ -108,6 +181,8 @@ pub struct Tablet {
|
|||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
||||||
pub struct Output {
|
pub struct Output {
|
||||||
|
#[knuffel(child)]
|
||||||
|
pub off: bool,
|
||||||
#[knuffel(argument)]
|
#[knuffel(argument)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[knuffel(child, unwrap(argument), default = 1.)]
|
#[knuffel(child, unwrap(argument), default = 1.)]
|
||||||
@@ -121,6 +196,7 @@ pub struct Output {
|
|||||||
impl Default for Output {
|
impl Default for Output {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
off: false,
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
scale: 1.,
|
scale: 1.,
|
||||||
position: None,
|
position: None,
|
||||||
@@ -129,7 +205,7 @@ impl Default for Output {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
|
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Position {
|
pub struct Position {
|
||||||
#[knuffel(property)]
|
#[knuffel(property)]
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
@@ -137,13 +213,31 @@ pub struct Position {
|
|||||||
pub y: i32,
|
pub y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Mode {
|
pub struct Mode {
|
||||||
pub width: u16,
|
pub width: u16,
|
||||||
pub height: u16,
|
pub height: u16,
|
||||||
pub refresh: Option<f64>,
|
pub refresh: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct Layout {
|
||||||
|
#[knuffel(child, default)]
|
||||||
|
pub focus_ring: FocusRing,
|
||||||
|
#[knuffel(child, default = default_border())]
|
||||||
|
pub border: FocusRing,
|
||||||
|
#[knuffel(child, unwrap(children), default)]
|
||||||
|
pub preset_column_widths: Vec<PresetWidth>,
|
||||||
|
#[knuffel(child)]
|
||||||
|
pub default_column_width: Option<DefaultColumnWidth>,
|
||||||
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
|
pub center_focused_column: CenterFocusedColumn,
|
||||||
|
#[knuffel(child, unwrap(argument), default = 16)]
|
||||||
|
pub gaps: u16,
|
||||||
|
#[knuffel(child, default)]
|
||||||
|
pub struts: Struts,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
|
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SpawnAtStartup {
|
pub struct SpawnAtStartup {
|
||||||
#[knuffel(arguments)]
|
#[knuffel(arguments)]
|
||||||
@@ -173,6 +267,15 @@ impl Default for FocusRing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn default_border() -> FocusRing {
|
||||||
|
FocusRing {
|
||||||
|
off: true,
|
||||||
|
width: 4,
|
||||||
|
active_color: Color::new(255, 200, 127, 255),
|
||||||
|
inactive_color: Color::new(80, 80, 80, 255),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
#[knuffel(argument)]
|
#[knuffel(argument)]
|
||||||
@@ -186,7 +289,7 @@ pub struct Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Color {
|
impl Color {
|
||||||
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
Self { r, g, b, a }
|
Self { r, g, b, a }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +326,24 @@ pub enum PresetWidth {
|
|||||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
||||||
pub struct DefaultColumnWidth(#[knuffel(children)] pub Vec<PresetWidth>);
|
pub struct DefaultColumnWidth(#[knuffel(children)] pub Vec<PresetWidth>);
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Struts {
|
||||||
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
|
pub left: u16,
|
||||||
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
|
pub right: u16,
|
||||||
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
|
pub top: u16,
|
||||||
|
#[knuffel(child, unwrap(argument), default)]
|
||||||
|
pub bottom: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct HotkeyOverlay {
|
||||||
|
#[knuffel(child)]
|
||||||
|
pub skip_at_startup: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||||
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
|
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
|
||||||
|
|
||||||
@@ -271,12 +392,20 @@ pub enum Action {
|
|||||||
FullscreenWindow,
|
FullscreenWindow,
|
||||||
FocusColumnLeft,
|
FocusColumnLeft,
|
||||||
FocusColumnRight,
|
FocusColumnRight,
|
||||||
|
FocusColumnFirst,
|
||||||
|
FocusColumnLast,
|
||||||
FocusWindowDown,
|
FocusWindowDown,
|
||||||
FocusWindowUp,
|
FocusWindowUp,
|
||||||
|
FocusWindowOrWorkspaceDown,
|
||||||
|
FocusWindowOrWorkspaceUp,
|
||||||
MoveColumnLeft,
|
MoveColumnLeft,
|
||||||
MoveColumnRight,
|
MoveColumnRight,
|
||||||
|
MoveColumnToFirst,
|
||||||
|
MoveColumnToLast,
|
||||||
MoveWindowDown,
|
MoveWindowDown,
|
||||||
MoveWindowUp,
|
MoveWindowUp,
|
||||||
|
MoveWindowDownOrToWorkspaceDown,
|
||||||
|
MoveWindowUpOrToWorkspaceUp,
|
||||||
ConsumeWindowIntoColumn,
|
ConsumeWindowIntoColumn,
|
||||||
ExpelWindowFromColumn,
|
ExpelWindowFromColumn,
|
||||||
CenterColumn,
|
CenterColumn,
|
||||||
@@ -286,6 +415,9 @@ pub enum Action {
|
|||||||
MoveWindowToWorkspaceDown,
|
MoveWindowToWorkspaceDown,
|
||||||
MoveWindowToWorkspaceUp,
|
MoveWindowToWorkspaceUp,
|
||||||
MoveWindowToWorkspace(#[knuffel(argument)] u8),
|
MoveWindowToWorkspace(#[knuffel(argument)] u8),
|
||||||
|
MoveColumnToWorkspaceDown,
|
||||||
|
MoveColumnToWorkspaceUp,
|
||||||
|
MoveColumnToWorkspace(#[knuffel(argument)] u8),
|
||||||
MoveWorkspaceDown,
|
MoveWorkspaceDown,
|
||||||
MoveWorkspaceUp,
|
MoveWorkspaceUp,
|
||||||
FocusMonitorLeft,
|
FocusMonitorLeft,
|
||||||
@@ -296,11 +428,16 @@ pub enum Action {
|
|||||||
MoveWindowToMonitorRight,
|
MoveWindowToMonitorRight,
|
||||||
MoveWindowToMonitorDown,
|
MoveWindowToMonitorDown,
|
||||||
MoveWindowToMonitorUp,
|
MoveWindowToMonitorUp,
|
||||||
|
MoveColumnToMonitorLeft,
|
||||||
|
MoveColumnToMonitorRight,
|
||||||
|
MoveColumnToMonitorDown,
|
||||||
|
MoveColumnToMonitorUp,
|
||||||
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
|
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
|
||||||
SwitchPresetColumnWidth,
|
SwitchPresetColumnWidth,
|
||||||
MaximizeColumn,
|
MaximizeColumn,
|
||||||
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
|
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
|
||||||
SwitchLayout(#[knuffel(argument)] LayoutAction),
|
SwitchLayout(#[knuffel(argument)] LayoutAction),
|
||||||
|
ShowHotkeyOverlay,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -329,6 +466,10 @@ pub struct DebugConfig {
|
|||||||
pub enable_color_transformations_capability: bool,
|
pub enable_color_transformations_capability: bool,
|
||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
pub enable_overlay_planes: bool,
|
pub enable_overlay_planes: bool,
|
||||||
|
#[knuffel(child)]
|
||||||
|
pub disable_cursor_plane: bool,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
pub render_drm_device: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DebugConfig {
|
impl Default for DebugConfig {
|
||||||
@@ -339,33 +480,30 @@ impl Default for DebugConfig {
|
|||||||
wait_for_frame_completion_before_queueing: false,
|
wait_for_frame_completion_before_queueing: false,
|
||||||
enable_color_transformations_capability: false,
|
enable_color_transformations_capability: false,
|
||||||
enable_overlay_planes: false,
|
enable_overlay_planes: false,
|
||||||
|
disable_cursor_plane: false,
|
||||||
|
render_drm_device: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> {
|
pub fn load(path: &Path) -> miette::Result<Self> {
|
||||||
let path = if let Some(path) = path {
|
let _span = tracy_client::span!("Config::load");
|
||||||
path
|
Self::load_internal(path).context("error loading config")
|
||||||
} else {
|
}
|
||||||
let mut path = ProjectDirs::from("", "", "niri")
|
|
||||||
.ok_or_else(|| miette!("error retrieving home directory"))?
|
|
||||||
.config_dir()
|
|
||||||
.to_owned();
|
|
||||||
path.push("config.kdl");
|
|
||||||
path
|
|
||||||
};
|
|
||||||
|
|
||||||
let contents = std::fs::read_to_string(&path)
|
fn load_internal(path: &Path) -> miette::Result<Self> {
|
||||||
|
let contents = std::fs::read_to_string(path)
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.with_context(|| format!("error reading {path:?}"))?;
|
.with_context(|| format!("error reading {path:?}"))?;
|
||||||
|
|
||||||
let config = Self::parse("config.kdl", &contents).context("error parsing")?;
|
let config = Self::parse("config.kdl", &contents).context("error parsing")?;
|
||||||
debug!("loaded config from {path:?}");
|
debug!("loaded config from {path:?}");
|
||||||
Ok((config, path))
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
|
pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
|
||||||
|
let _span = tracy_client::span!("Config::parse");
|
||||||
knuffel::parse(filename, text)
|
knuffel::parse(filename, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,7 +512,7 @@ impl Default for Config {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Config::parse(
|
Config::parse(
|
||||||
"default-config.kdl",
|
"default-config.kdl",
|
||||||
include_str!("../resources/default-config.kdl"),
|
include_str!("../../resources/default-config.kdl"),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
@@ -502,6 +640,38 @@ impl FromStr for SizeChange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for AccelProfile {
|
||||||
|
type Err = miette::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"adaptive" => Ok(Self::Adaptive),
|
||||||
|
"flat" => Ok(Self::Flat),
|
||||||
|
_ => Err(miette!(
|
||||||
|
r#"invalid accel profile, can be "adaptive" or "flat""#
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TapButtonMap {
|
||||||
|
type Err = miette::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"left-right-middle" => Ok(Self::LeftRightMiddle),
|
||||||
|
"left-middle-right" => Ok(Self::LeftMiddleRight),
|
||||||
|
_ => Err(miette!(
|
||||||
|
r#"invalid tap button map, can be "left-right-middle" or "left-middle-right""#
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_miette_hook() -> Result<(), miette::InstallError> {
|
||||||
|
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new())))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use miette::NarratableReportHandler;
|
use miette::NarratableReportHandler;
|
||||||
@@ -535,12 +705,23 @@ mod tests {
|
|||||||
|
|
||||||
touchpad {
|
touchpad {
|
||||||
tap
|
tap
|
||||||
|
dwt
|
||||||
accel-speed 0.2
|
accel-speed 0.2
|
||||||
|
accel-profile "flat"
|
||||||
|
tap-button-map "left-middle-right"
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse {
|
||||||
|
natural-scroll
|
||||||
|
accel-speed 0.4
|
||||||
|
accel-profile "flat"
|
||||||
}
|
}
|
||||||
|
|
||||||
tablet {
|
tablet {
|
||||||
map-to-output "eDP-1"
|
map-to-output "eDP-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disable-power-key-handling
|
||||||
}
|
}
|
||||||
|
|
||||||
output "eDP-1" {
|
output "eDP-1" {
|
||||||
@@ -549,14 +730,41 @@ mod tests {
|
|||||||
mode "1920x1080@144"
|
mode "1920x1080@144"
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn-at-startup "alacritty" "-e" "fish"
|
layout {
|
||||||
|
focus-ring {
|
||||||
|
width 5
|
||||||
|
active-color 0 100 200 255
|
||||||
|
inactive-color 255 200 100 0
|
||||||
|
}
|
||||||
|
|
||||||
focus-ring {
|
border {
|
||||||
width 5
|
width 3
|
||||||
active-color 0 100 200 255
|
active-color 0 100 200 255
|
||||||
inactive-color 255 200 100 0
|
inactive-color 255 200 100 0
|
||||||
|
}
|
||||||
|
|
||||||
|
preset-column-widths {
|
||||||
|
proportion 0.25
|
||||||
|
proportion 0.5
|
||||||
|
fixed 960
|
||||||
|
fixed 1280
|
||||||
|
}
|
||||||
|
|
||||||
|
default-column-width { proportion 0.25; }
|
||||||
|
|
||||||
|
gaps 8
|
||||||
|
|
||||||
|
struts {
|
||||||
|
left 1
|
||||||
|
right 2
|
||||||
|
top 3
|
||||||
|
}
|
||||||
|
|
||||||
|
center-focused-column "on-overflow"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawn-at-startup "alacritty" "-e" "fish"
|
||||||
|
|
||||||
prefer-no-csd
|
prefer-no-csd
|
||||||
|
|
||||||
cursor {
|
cursor {
|
||||||
@@ -564,19 +772,12 @@ mod tests {
|
|||||||
xcursor-size 16
|
xcursor-size 16
|
||||||
}
|
}
|
||||||
|
|
||||||
preset-column-widths {
|
|
||||||
proportion 0.25
|
|
||||||
proportion 0.5
|
|
||||||
fixed 960
|
|
||||||
fixed 1280
|
|
||||||
}
|
|
||||||
|
|
||||||
default-column-width { proportion 0.25; }
|
|
||||||
|
|
||||||
gaps 8
|
|
||||||
|
|
||||||
screenshot-path "~/Screenshots/screenshot.png"
|
screenshot-path "~/Screenshots/screenshot.png"
|
||||||
|
|
||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
|
||||||
binds {
|
binds {
|
||||||
Mod+T { spawn "alacritty"; }
|
Mod+T { spawn "alacritty"; }
|
||||||
Mod+Q { close-window; }
|
Mod+Q { close-window; }
|
||||||
@@ -588,6 +789,7 @@ mod tests {
|
|||||||
|
|
||||||
debug {
|
debug {
|
||||||
animation-slowdown 2.0
|
animation-slowdown 2.0
|
||||||
|
render-drm-device "/dev/dri/renderD129"
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
Config {
|
Config {
|
||||||
@@ -604,14 +806,24 @@ mod tests {
|
|||||||
},
|
},
|
||||||
touchpad: Touchpad {
|
touchpad: Touchpad {
|
||||||
tap: true,
|
tap: true,
|
||||||
|
dwt: true,
|
||||||
natural_scroll: false,
|
natural_scroll: false,
|
||||||
accel_speed: 0.2,
|
accel_speed: 0.2,
|
||||||
|
accel_profile: Some(AccelProfile::Flat),
|
||||||
|
tap_button_map: Some(TapButtonMap::LeftMiddleRight),
|
||||||
|
},
|
||||||
|
mouse: Mouse {
|
||||||
|
natural_scroll: true,
|
||||||
|
accel_speed: 0.4,
|
||||||
|
accel_profile: Some(AccelProfile::Flat),
|
||||||
},
|
},
|
||||||
tablet: Tablet {
|
tablet: Tablet {
|
||||||
map_to_output: Some("eDP-1".to_owned()),
|
map_to_output: Some("eDP-1".to_owned()),
|
||||||
},
|
},
|
||||||
|
disable_power_key_handling: true,
|
||||||
},
|
},
|
||||||
outputs: vec![Output {
|
outputs: vec![Output {
|
||||||
|
off: false,
|
||||||
name: "eDP-1".to_owned(),
|
name: "eDP-1".to_owned(),
|
||||||
scale: 2.,
|
scale: 2.,
|
||||||
position: Some(Position { x: 10, y: 20 }),
|
position: Some(Position { x: 10, y: 20 }),
|
||||||
@@ -621,39 +833,69 @@ mod tests {
|
|||||||
refresh: Some(144.),
|
refresh: Some(144.),
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
layout: Layout {
|
||||||
|
focus_ring: FocusRing {
|
||||||
|
off: false,
|
||||||
|
width: 5,
|
||||||
|
active_color: Color {
|
||||||
|
r: 0,
|
||||||
|
g: 100,
|
||||||
|
b: 200,
|
||||||
|
a: 255,
|
||||||
|
},
|
||||||
|
inactive_color: Color {
|
||||||
|
r: 255,
|
||||||
|
g: 200,
|
||||||
|
b: 100,
|
||||||
|
a: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
border: FocusRing {
|
||||||
|
off: false,
|
||||||
|
width: 3,
|
||||||
|
active_color: Color {
|
||||||
|
r: 0,
|
||||||
|
g: 100,
|
||||||
|
b: 200,
|
||||||
|
a: 255,
|
||||||
|
},
|
||||||
|
inactive_color: Color {
|
||||||
|
r: 255,
|
||||||
|
g: 200,
|
||||||
|
b: 100,
|
||||||
|
a: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preset_column_widths: vec![
|
||||||
|
PresetWidth::Proportion(0.25),
|
||||||
|
PresetWidth::Proportion(0.5),
|
||||||
|
PresetWidth::Fixed(960),
|
||||||
|
PresetWidth::Fixed(1280),
|
||||||
|
],
|
||||||
|
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(
|
||||||
|
0.25,
|
||||||
|
)])),
|
||||||
|
gaps: 8,
|
||||||
|
struts: Struts {
|
||||||
|
left: 1,
|
||||||
|
right: 2,
|
||||||
|
top: 3,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
center_focused_column: CenterFocusedColumn::OnOverflow,
|
||||||
|
},
|
||||||
spawn_at_startup: vec![SpawnAtStartup {
|
spawn_at_startup: vec![SpawnAtStartup {
|
||||||
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
|
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
|
||||||
}],
|
}],
|
||||||
focus_ring: FocusRing {
|
|
||||||
off: false,
|
|
||||||
width: 5,
|
|
||||||
active_color: Color {
|
|
||||||
r: 0,
|
|
||||||
g: 100,
|
|
||||||
b: 200,
|
|
||||||
a: 255,
|
|
||||||
},
|
|
||||||
inactive_color: Color {
|
|
||||||
r: 255,
|
|
||||||
g: 200,
|
|
||||||
b: 100,
|
|
||||||
a: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
prefer_no_csd: true,
|
prefer_no_csd: true,
|
||||||
cursor: Cursor {
|
cursor: Cursor {
|
||||||
xcursor_theme: String::from("breeze_cursors"),
|
xcursor_theme: String::from("breeze_cursors"),
|
||||||
xcursor_size: 16,
|
xcursor_size: 16,
|
||||||
},
|
},
|
||||||
preset_column_widths: vec![
|
|
||||||
PresetWidth::Proportion(0.25),
|
|
||||||
PresetWidth::Proportion(0.5),
|
|
||||||
PresetWidth::Fixed(960),
|
|
||||||
PresetWidth::Fixed(1280),
|
|
||||||
],
|
|
||||||
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(0.25)])),
|
|
||||||
gaps: 8,
|
|
||||||
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
|
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
|
||||||
|
hotkey_overlay: HotkeyOverlay {
|
||||||
|
skip_at_startup: true,
|
||||||
|
},
|
||||||
binds: Binds(vec![
|
binds: Binds(vec![
|
||||||
Bind {
|
Bind {
|
||||||
key: Key {
|
key: Key {
|
||||||
@@ -700,6 +942,7 @@ mod tests {
|
|||||||
]),
|
]),
|
||||||
debug: DebugConfig {
|
debug: DebugConfig {
|
||||||
animation_slowdown: 2.,
|
animation_slowdown: 2.,
|
||||||
|
render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "niri-ipc"
|
||||||
|
version.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde.workspace = true
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//! Types for communicating with niri via IPC.
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Name of the environment variable containing the niri IPC socket path.
|
||||||
|
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||||
|
|
||||||
|
/// Request from client to niri.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Request {
|
||||||
|
/// Request information about connected outputs.
|
||||||
|
Outputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response from niri to client.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Response {
|
||||||
|
/// Information about connected outputs.
|
||||||
|
///
|
||||||
|
/// Map from connector name to output info.
|
||||||
|
Outputs(HashMap<String, Output>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connected output.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Output {
|
||||||
|
/// Name of the output.
|
||||||
|
pub name: String,
|
||||||
|
/// Textual description of the manufacturer.
|
||||||
|
pub make: String,
|
||||||
|
/// Textual description of the model.
|
||||||
|
pub model: String,
|
||||||
|
/// Physical width and height of the output in millimeters, if known.
|
||||||
|
pub physical_size: Option<(u32, u32)>,
|
||||||
|
/// Available modes for the output.
|
||||||
|
pub modes: Vec<Mode>,
|
||||||
|
/// Index of the current mode in [`Self::modes`].
|
||||||
|
///
|
||||||
|
/// `None` if the output is disabled.
|
||||||
|
pub current_mode: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output mode.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||||
|
pub struct Mode {
|
||||||
|
/// Width in physical pixels.
|
||||||
|
pub width: u16,
|
||||||
|
/// Height in physical pixels.
|
||||||
|
pub height: u16,
|
||||||
|
/// Refresh rate in millihertz.
|
||||||
|
pub refresh_rate: u32,
|
||||||
|
}
|
||||||
+190
-75
@@ -27,8 +27,17 @@ input {
|
|||||||
// Omitting settings disables them, or leaves them at their default values.
|
// Omitting settings disables them, or leaves them at their default values.
|
||||||
touchpad {
|
touchpad {
|
||||||
tap
|
tap
|
||||||
|
// dwt
|
||||||
natural-scroll
|
natural-scroll
|
||||||
// accel-speed 0.2
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// tap-button-map "left-middle-right"
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse {
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
}
|
}
|
||||||
|
|
||||||
tablet {
|
tablet {
|
||||||
@@ -37,12 +46,22 @@ input {
|
|||||||
// existing outputs.
|
// existing outputs.
|
||||||
map-to-output "eDP-1"
|
map-to-output "eDP-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// By default, niri will take over the power button to make it sleep
|
||||||
|
// instead of power off.
|
||||||
|
// Uncomment this if you would like to configure the power button elsewhere
|
||||||
|
// (i.e. logind.conf).
|
||||||
|
// disable-power-key-handling
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can configure outputs by their name, which you can find with wayland-info(1).
|
// You can configure outputs by their name, which you can find
|
||||||
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
// The built-in laptop monitor is usually called "eDP-1".
|
// The built-in laptop monitor is usually called "eDP-1".
|
||||||
// Remember to uncommend the node by removing "/-"!
|
// Remember to uncommend the node by removing "/-"!
|
||||||
/-output "eDP-1" {
|
/-output "eDP-1" {
|
||||||
|
// Uncomment this line to disable this output.
|
||||||
|
// off
|
||||||
|
|
||||||
// Scale is a floating-point number, but at the moment only integer values work.
|
// Scale is a floating-point number, but at the moment only integer values work.
|
||||||
scale 2.0
|
scale 2.0
|
||||||
|
|
||||||
@@ -51,7 +70,7 @@ input {
|
|||||||
// If the refresh rate is omitted, niri will pick the highest refresh rate
|
// If the refresh rate is omitted, niri will pick the highest refresh rate
|
||||||
// for the resolution.
|
// for the resolution.
|
||||||
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
|
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
|
||||||
// All valid modes are listed in niri's debug output when an output is connected.
|
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
|
||||||
mode "1920x1080@144"
|
mode "1920x1080@144"
|
||||||
|
|
||||||
// Position of the output in the global coordinate space.
|
// Position of the output in the global coordinate space.
|
||||||
@@ -66,26 +85,80 @@ input {
|
|||||||
position x=1280 y=0
|
position x=1280 y=0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layout {
|
||||||
|
// You can change how the focus ring looks.
|
||||||
|
focus-ring {
|
||||||
|
// Uncomment this line to disable the focus ring.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// How many logical pixels the ring extends out from the windows.
|
||||||
|
width 4
|
||||||
|
|
||||||
|
// Color of the ring on the active monitor: red, green, blue, alpha.
|
||||||
|
active-color 127 200 255 255
|
||||||
|
|
||||||
|
// Color of the ring on inactive monitors: red, green, blue, alpha.
|
||||||
|
inactive-color 80 80 80 255
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can also add a border. It's similar to the focus ring, but always visible.
|
||||||
|
border {
|
||||||
|
// The settings are the same as for the focus ring.
|
||||||
|
// If you enable the border, you probably want to disable the focus ring.
|
||||||
|
off
|
||||||
|
|
||||||
|
width 4
|
||||||
|
active-color 255 200 127 255
|
||||||
|
inactive-color 80 80 80 255
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||||
|
preset-column-widths {
|
||||||
|
// 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.
|
||||||
|
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
||||||
|
proportion 0.33333
|
||||||
|
proportion 0.5
|
||||||
|
proportion 0.66667
|
||||||
|
|
||||||
|
// Fixed sets the width in logical pixels exactly.
|
||||||
|
// fixed 1920
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can change the default width of the new windows.
|
||||||
|
default-column-width { proportion 0.5; }
|
||||||
|
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||||
|
// default-column-width {}
|
||||||
|
|
||||||
|
// Set gaps around windows in logical pixels.
|
||||||
|
gaps 16
|
||||||
|
|
||||||
|
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||||
|
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
||||||
|
// Left and right struts will cause the next window to the side to always be visible.
|
||||||
|
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
|
||||||
|
// layer-shell panels and regular gaps.
|
||||||
|
struts {
|
||||||
|
// left 64
|
||||||
|
// right 64
|
||||||
|
// top 64
|
||||||
|
// bottom 64
|
||||||
|
}
|
||||||
|
|
||||||
|
// When to center a column when changing focus, options are:
|
||||||
|
// - "never", default behavior, focusing an off-screen column will keep at the left
|
||||||
|
// or right edge of the screen.
|
||||||
|
// - "on-overflow", focusing a column will center it if it doesn't fit
|
||||||
|
// together with the previously focused column.
|
||||||
|
// - "always", the focused column will always be centered.
|
||||||
|
center-focused-column "never"
|
||||||
|
}
|
||||||
|
|
||||||
// Add lines like this to spawn processes at startup.
|
// Add lines like this to spawn processes at startup.
|
||||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||||
// which may be more convenient to use.
|
// which may be more convenient to use.
|
||||||
// spawn-at-startup "alacritty" "-e" "fish"
|
// spawn-at-startup "alacritty" "-e" "fish"
|
||||||
|
|
||||||
// You can change how the focus ring looks.
|
|
||||||
focus-ring {
|
|
||||||
// Uncomment this line to disable the focus ring.
|
|
||||||
// off
|
|
||||||
|
|
||||||
// How many logical pixels the ring extends out from the windows.
|
|
||||||
width 4
|
|
||||||
|
|
||||||
// Color of the ring on the active monitor: red, green, blue, alpha.
|
|
||||||
active-color 127 200 255 255
|
|
||||||
|
|
||||||
// Color of the ring on inactive monitors: red, green, blue, alpha.
|
|
||||||
inactive-color 80 80 80 255
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor {
|
cursor {
|
||||||
// Change the theme and size of the cursor as well as set the
|
// Change the theme and size of the cursor as well as set the
|
||||||
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
|
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
|
||||||
@@ -98,27 +171,6 @@ cursor {
|
|||||||
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||||
// prefer-no-csd
|
// prefer-no-csd
|
||||||
|
|
||||||
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
|
||||||
preset-column-widths {
|
|
||||||
// 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.
|
|
||||||
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
|
||||||
proportion 0.333
|
|
||||||
proportion 0.5
|
|
||||||
proportion 0.667
|
|
||||||
|
|
||||||
// Fixed sets the width in logical pixels exactly.
|
|
||||||
// fixed 1920
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can change the default width of the new windows.
|
|
||||||
default-column-width { proportion 0.5; }
|
|
||||||
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
|
||||||
// default-column-width {}
|
|
||||||
|
|
||||||
// Set gaps around windows in logical pixels.
|
|
||||||
gaps 16
|
|
||||||
|
|
||||||
// You can change the path where screenshots are saved.
|
// You can change the path where screenshots are saved.
|
||||||
// A ~ at the front will be expanded to the home directory.
|
// A ~ at the front will be expanded to the home directory.
|
||||||
// The path is formatted with strftime(3) to give you the screenshot date and time.
|
// The path is formatted with strftime(3) to give you the screenshot date and time.
|
||||||
@@ -127,6 +179,12 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
|||||||
// You can also set this to null to disable saving screenshots to disk.
|
// You can also set this to null to disable saving screenshots to disk.
|
||||||
// screenshot-path null
|
// screenshot-path null
|
||||||
|
|
||||||
|
// Settings for the "Important Hotkeys" overlay.
|
||||||
|
hotkey-overlay {
|
||||||
|
// Uncomment this line if you don't want to see the hotkey help at niri startup.
|
||||||
|
// skip-at-startup
|
||||||
|
}
|
||||||
|
|
||||||
binds {
|
binds {
|
||||||
// Keys consist of modifiers separated by + signs, followed by an XKB key name
|
// Keys consist of modifiers separated by + signs, followed by an XKB key name
|
||||||
// in the end. To find an XKB name for a particular key, you may use a program
|
// in the end. To find an XKB name for a particular key, you may use a program
|
||||||
@@ -135,66 +193,93 @@ binds {
|
|||||||
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
|
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
|
||||||
// when running as a winit window.
|
// when running as a winit window.
|
||||||
|
|
||||||
|
// Mod-Shift-/, which is usually the same as Mod-?,
|
||||||
|
// shows a list of important hotkeys.
|
||||||
|
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||||
|
|
||||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||||
Mod+T { spawn "alacritty"; }
|
Mod+T { spawn "alacritty"; }
|
||||||
Mod+D { spawn "fuzzel"; }
|
Mod+D { spawn "fuzzel"; }
|
||||||
Mod+Alt+L { spawn "swaylock"; }
|
Mod+Alt+L { spawn "swaylock"; }
|
||||||
|
|
||||||
|
// You can also use a shell:
|
||||||
|
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
|
||||||
|
|
||||||
// Example volume keys mappings for PipeWire & WirePlumber.
|
// Example volume keys mappings for PipeWire & WirePlumber.
|
||||||
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||||
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
||||||
|
|
||||||
Mod+Q { close-window; }
|
Mod+Q { close-window; }
|
||||||
|
|
||||||
Mod+H { focus-column-left; }
|
|
||||||
Mod+J { focus-window-down; }
|
|
||||||
Mod+K { focus-window-up; }
|
|
||||||
Mod+L { focus-column-right; }
|
|
||||||
Mod+Left { focus-column-left; }
|
Mod+Left { focus-column-left; }
|
||||||
Mod+Down { focus-window-down; }
|
Mod+Down { focus-window-down; }
|
||||||
Mod+Up { focus-window-up; }
|
Mod+Up { focus-window-up; }
|
||||||
Mod+Right { focus-column-right; }
|
Mod+Right { focus-column-right; }
|
||||||
|
Mod+H { focus-column-left; }
|
||||||
|
Mod+J { focus-window-down; }
|
||||||
|
Mod+K { focus-window-up; }
|
||||||
|
Mod+L { focus-column-right; }
|
||||||
|
|
||||||
Mod+Ctrl+H { move-column-left; }
|
|
||||||
Mod+Ctrl+J { move-window-down; }
|
|
||||||
Mod+Ctrl+K { move-window-up; }
|
|
||||||
Mod+Ctrl+L { move-column-right; }
|
|
||||||
Mod+Ctrl+Left { move-column-left; }
|
Mod+Ctrl+Left { move-column-left; }
|
||||||
Mod+Ctrl+Down { move-window-down; }
|
Mod+Ctrl+Down { move-window-down; }
|
||||||
Mod+Ctrl+Up { move-window-up; }
|
Mod+Ctrl+Up { move-window-up; }
|
||||||
Mod+Ctrl+Right { move-column-right; }
|
Mod+Ctrl+Right { move-column-right; }
|
||||||
|
Mod+Ctrl+H { move-column-left; }
|
||||||
|
Mod+Ctrl+J { move-window-down; }
|
||||||
|
Mod+Ctrl+K { move-window-up; }
|
||||||
|
Mod+Ctrl+L { move-column-right; }
|
||||||
|
|
||||||
|
// Alternative commands that move across workspaces when reaching
|
||||||
|
// the first or last window in a column.
|
||||||
|
// Mod+J { focus-window-or-workspace-down; }
|
||||||
|
// Mod+K { focus-window-or-workspace-up; }
|
||||||
|
// Mod+Ctrl+J { move-window-down-or-to-workspace-down; }
|
||||||
|
// Mod+Ctrl+K { move-window-up-or-to-workspace-up; }
|
||||||
|
|
||||||
|
Mod+Home { focus-column-first; }
|
||||||
|
Mod+End { focus-column-last; }
|
||||||
|
Mod+Ctrl+Home { move-column-to-first; }
|
||||||
|
Mod+Ctrl+End { move-column-to-last; }
|
||||||
|
|
||||||
Mod+Shift+H { focus-monitor-left; }
|
|
||||||
Mod+Shift+J { focus-monitor-down; }
|
|
||||||
Mod+Shift+K { focus-monitor-up; }
|
|
||||||
Mod+Shift+L { focus-monitor-right; }
|
|
||||||
Mod+Shift+Left { focus-monitor-left; }
|
Mod+Shift+Left { focus-monitor-left; }
|
||||||
Mod+Shift+Down { focus-monitor-down; }
|
Mod+Shift+Down { focus-monitor-down; }
|
||||||
Mod+Shift+Up { focus-monitor-up; }
|
Mod+Shift+Up { focus-monitor-up; }
|
||||||
Mod+Shift+Right { focus-monitor-right; }
|
Mod+Shift+Right { focus-monitor-right; }
|
||||||
|
Mod+Shift+H { focus-monitor-left; }
|
||||||
|
Mod+Shift+J { focus-monitor-down; }
|
||||||
|
Mod+Shift+K { focus-monitor-up; }
|
||||||
|
Mod+Shift+L { focus-monitor-right; }
|
||||||
|
|
||||||
Mod+Shift+Ctrl+H { move-window-to-monitor-left; }
|
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||||
Mod+Shift+Ctrl+J { move-window-to-monitor-down; }
|
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||||
Mod+Shift+Ctrl+K { move-window-to-monitor-up; }
|
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||||
Mod+Shift+Ctrl+L { move-window-to-monitor-right; }
|
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||||
Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
|
||||||
Mod+Shift+Ctrl+Down { move-window-to-monitor-down; }
|
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
||||||
Mod+Shift+Ctrl+Up { move-window-to-monitor-up; }
|
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
||||||
Mod+Shift+Ctrl+Right { move-window-to-monitor-right; }
|
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
|
||||||
|
|
||||||
|
// Alternatively, there are commands to move just a single window:
|
||||||
|
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
||||||
|
// ...
|
||||||
|
|
||||||
Mod+U { focus-workspace-down; }
|
|
||||||
Mod+I { focus-workspace-up; }
|
|
||||||
Mod+Page_Down { focus-workspace-down; }
|
Mod+Page_Down { focus-workspace-down; }
|
||||||
Mod+Page_Up { focus-workspace-up; }
|
Mod+Page_Up { focus-workspace-up; }
|
||||||
Mod+Ctrl+U { move-window-to-workspace-down; }
|
Mod+U { focus-workspace-down; }
|
||||||
Mod+Ctrl+I { move-window-to-workspace-up; }
|
Mod+I { focus-workspace-up; }
|
||||||
Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
|
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
|
||||||
Mod+Ctrl+Page_Up { move-window-to-workspace-up; }
|
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
|
||||||
|
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
// Alternatively, there are commands to move just a single window:
|
||||||
|
// Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
|
||||||
|
// ...
|
||||||
|
|
||||||
Mod+Shift+U { move-workspace-down; }
|
|
||||||
Mod+Shift+I { move-workspace-up; }
|
|
||||||
Mod+Shift+Page_Down { move-workspace-down; }
|
Mod+Shift+Page_Down { move-workspace-down; }
|
||||||
Mod+Shift+Page_Up { move-workspace-up; }
|
Mod+Shift+Page_Up { move-workspace-up; }
|
||||||
|
Mod+Shift+U { move-workspace-down; }
|
||||||
|
Mod+Shift+I { move-workspace-up; }
|
||||||
|
|
||||||
Mod+1 { focus-workspace 1; }
|
Mod+1 { focus-workspace 1; }
|
||||||
Mod+2 { focus-workspace 2; }
|
Mod+2 { focus-workspace 2; }
|
||||||
@@ -205,15 +290,18 @@ binds {
|
|||||||
Mod+7 { focus-workspace 7; }
|
Mod+7 { focus-workspace 7; }
|
||||||
Mod+8 { focus-workspace 8; }
|
Mod+8 { focus-workspace 8; }
|
||||||
Mod+9 { focus-workspace 9; }
|
Mod+9 { focus-workspace 9; }
|
||||||
Mod+Ctrl+1 { move-window-to-workspace 1; }
|
Mod+Ctrl+1 { move-column-to-workspace 1; }
|
||||||
Mod+Ctrl+2 { move-window-to-workspace 2; }
|
Mod+Ctrl+2 { move-column-to-workspace 2; }
|
||||||
Mod+Ctrl+3 { move-window-to-workspace 3; }
|
Mod+Ctrl+3 { move-column-to-workspace 3; }
|
||||||
Mod+Ctrl+4 { move-window-to-workspace 4; }
|
Mod+Ctrl+4 { move-column-to-workspace 4; }
|
||||||
Mod+Ctrl+5 { move-window-to-workspace 5; }
|
Mod+Ctrl+5 { move-column-to-workspace 5; }
|
||||||
Mod+Ctrl+6 { move-window-to-workspace 6; }
|
Mod+Ctrl+6 { move-column-to-workspace 6; }
|
||||||
Mod+Ctrl+7 { move-window-to-workspace 7; }
|
Mod+Ctrl+7 { move-column-to-workspace 7; }
|
||||||
Mod+Ctrl+8 { move-window-to-workspace 8; }
|
Mod+Ctrl+8 { move-column-to-workspace 8; }
|
||||||
Mod+Ctrl+9 { move-window-to-workspace 9; }
|
Mod+Ctrl+9 { move-column-to-workspace 9; }
|
||||||
|
|
||||||
|
// Alternatively, there are commands to move just a single window:
|
||||||
|
// Mod+Ctrl+1 { move-window-to-workspace 1; }
|
||||||
|
|
||||||
Mod+Comma { consume-window-into-column; }
|
Mod+Comma { consume-window-into-column; }
|
||||||
Mod+Period { expel-window-from-column; }
|
Mod+Period { expel-window-from-column; }
|
||||||
@@ -255,3 +343,30 @@ binds {
|
|||||||
|
|
||||||
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings for debugging. Not meant for normal use.
|
||||||
|
// These can change or stop working at any point with little notice.
|
||||||
|
debug {
|
||||||
|
// Make niri take over its DBus services even if it's not running as a session.
|
||||||
|
// Useful for testing screen recording changes without having to relogin.
|
||||||
|
// The main niri instance will *not* currently take back the services; so you will
|
||||||
|
// need to relogin in the end.
|
||||||
|
// dbus-interfaces-in-non-session-instances
|
||||||
|
|
||||||
|
// Wait until every frame is done rendering before handing it over to DRM.
|
||||||
|
// wait-for-frame-completion-before-queueing
|
||||||
|
|
||||||
|
// Enable direct scanout into overlay planes.
|
||||||
|
// May cause frame drops during some animations on some hardware.
|
||||||
|
// enable-overlay-planes
|
||||||
|
|
||||||
|
// Disable the use of the cursor plane.
|
||||||
|
// The cursor will be rendered together with the rest of the frame.
|
||||||
|
// disable-cursor-plane
|
||||||
|
|
||||||
|
// Slow down animations by this factor.
|
||||||
|
// animation-slowdown 3.0
|
||||||
|
|
||||||
|
// Override the DRM device that niri will use for all rendering.
|
||||||
|
// render-drm-device "/dev/dri/renderD129"
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,4 +50,9 @@ impl Animation {
|
|||||||
pub fn to(&self) -> f64 {
|
pub fn to(&self) -> f64 {
|
||||||
self.to
|
self.to
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn from(&self) -> f64 {
|
||||||
|
self.from
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-21
@@ -1,12 +1,15 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
|
||||||
use crate::input::CompositorMod;
|
use crate::input::CompositorMod;
|
||||||
use crate::niri::OutputRenderElements;
|
|
||||||
use crate::Niri;
|
use crate::Niri;
|
||||||
|
|
||||||
pub mod tty;
|
pub mod tty;
|
||||||
@@ -26,8 +29,8 @@ pub enum RenderResult {
|
|||||||
Submitted,
|
Submitted,
|
||||||
/// Rendering succeeded, but there was no damage.
|
/// Rendering succeeded, but there was no damage.
|
||||||
NoDamage,
|
NoDamage,
|
||||||
/// An error has occurred, the frame was not submitted.
|
/// The frame was not rendered and submitted, due to an error or otherwise.
|
||||||
Error,
|
Skipped,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
@@ -45,10 +48,13 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn renderer(&mut self) -> Option<&mut GlesRenderer> {
|
pub fn with_primary_renderer<T>(
|
||||||
|
&mut self,
|
||||||
|
f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.renderer(),
|
Backend::Tty(tty) => tty.with_primary_renderer(f),
|
||||||
Backend::Winit(winit) => Some(winit.renderer()),
|
Backend::Winit(winit) => winit.with_primary_renderer(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,12 +62,11 @@ impl Backend {
|
|||||||
&mut self,
|
&mut self,
|
||||||
niri: &mut Niri,
|
niri: &mut Niri,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
elements: &[OutputRenderElements<GlesRenderer>],
|
|
||||||
target_presentation_time: Duration,
|
target_presentation_time: Duration,
|
||||||
) -> RenderResult {
|
) -> RenderResult {
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.render(niri, output, elements, target_presentation_time),
|
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
|
||||||
Backend::Winit(winit) => winit.render(niri, output, elements),
|
Backend::Winit(winit) => winit.render(niri, output),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,11 +98,32 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(feature = "dbus"), allow(unused))]
|
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||||
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.connectors(),
|
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||||
Backend::Winit(winit) => winit.connectors(),
|
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn early_import(&mut self, surface: &WlSurface) {
|
||||||
|
match self {
|
||||||
|
Backend::Tty(tty) => tty.early_import(surface),
|
||||||
|
Backend::Winit(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
|
||||||
|
match self {
|
||||||
|
Backend::Tty(tty) => tty.ipc_outputs(),
|
||||||
|
Backend::Winit(winit) => winit.ipc_outputs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(feature = "dbus"), allow(unused))]
|
||||||
|
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
||||||
|
match self {
|
||||||
|
Backend::Tty(tty) => tty.enabled_outputs(),
|
||||||
|
Backend::Winit(winit) => winit.enabled_outputs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,18 +133,11 @@ impl Backend {
|
|||||||
) -> Option<smithay::backend::allocator::gbm::GbmDevice<smithay::backend::drm::DrmDeviceFd>>
|
) -> Option<smithay::backend::allocator::gbm::GbmDevice<smithay::backend::drm::DrmDeviceFd>>
|
||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.gbm_device(),
|
Backend::Tty(tty) => tty.primary_gbm_device(),
|
||||||
Backend::Winit(_) => None,
|
Backend::Winit(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_active(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Backend::Tty(tty) => tty.is_active(),
|
|
||||||
Backend::Winit(_) => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_monitors_active(&self, active: bool) {
|
pub fn set_monitors_active(&self, active: bool) {
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||||
@@ -126,6 +145,13 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
|
||||||
|
match self {
|
||||||
|
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
||||||
|
Backend::Winit(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tty(&mut self) -> &mut Tty {
|
pub fn tty(&mut self) -> &mut Tty {
|
||||||
if let Self::Tty(v) = self {
|
if let Self::Tty(v) = self {
|
||||||
v
|
v
|
||||||
|
|||||||
+809
-303
File diff suppressed because it is too large
Load Diff
+69
-44
@@ -5,11 +5,13 @@ use std::rc::Rc;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri_config::Config;
|
||||||
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::backend::renderer::{DebugFlags, Renderer};
|
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
||||||
use smithay::backend::winit::{self, WinitEvent, WinitGraphicsBackend};
|
use smithay::backend::winit::{self, WinitEvent, WinitGraphicsBackend};
|
||||||
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
|
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||||
use smithay::reexports::calloop::LoopHandle;
|
use smithay::reexports::calloop::LoopHandle;
|
||||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||||
use smithay::reexports::winit::dpi::LogicalSize;
|
use smithay::reexports::winit::dpi::LogicalSize;
|
||||||
@@ -17,8 +19,7 @@ use smithay::reexports::winit::window::WindowBuilder;
|
|||||||
use smithay::utils::Transform;
|
use smithay::utils::Transform;
|
||||||
|
|
||||||
use super::RenderResult;
|
use super::RenderResult;
|
||||||
use crate::config::Config;
|
use crate::niri::{RedrawState, State};
|
||||||
use crate::niri::{OutputRenderElements, RedrawState, State};
|
|
||||||
use crate::utils::get_monotonic_time;
|
use crate::utils::get_monotonic_time;
|
||||||
use crate::Niri;
|
use crate::Niri;
|
||||||
|
|
||||||
@@ -27,7 +28,8 @@ pub struct Winit {
|
|||||||
output: Output,
|
output: Output,
|
||||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||||
damage_tracker: OutputDamageTracker,
|
damage_tracker: OutputDamageTracker,
|
||||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||||
|
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Winit {
|
impl Winit {
|
||||||
@@ -38,14 +40,6 @@ impl Winit {
|
|||||||
.with_title("niri");
|
.with_title("niri");
|
||||||
let (backend, winit) = winit::init_from_builder(builder).unwrap();
|
let (backend, winit) = winit::init_from_builder(builder).unwrap();
|
||||||
|
|
||||||
let output_config = config
|
|
||||||
.borrow()
|
|
||||||
.outputs
|
|
||||||
.iter()
|
|
||||||
.find(|o| o.name == "winit")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let output = Output::new(
|
let output = Output::new(
|
||||||
"winit".to_string(),
|
"winit".to_string(),
|
||||||
PhysicalProperties {
|
PhysicalProperties {
|
||||||
@@ -60,16 +54,27 @@ impl Winit {
|
|||||||
size: backend.window_size(),
|
size: backend.window_size(),
|
||||||
refresh: 60_000,
|
refresh: 60_000,
|
||||||
};
|
};
|
||||||
let scale = output_config.scale.clamp(1., 10.).ceil() as i32;
|
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
|
||||||
output.change_current_state(
|
|
||||||
Some(mode),
|
|
||||||
Some(Transform::Flipped180),
|
|
||||||
Some(Scale::Integer(scale)),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
output.set_preferred(mode);
|
output.set_preferred(mode);
|
||||||
|
|
||||||
let connectors = Arc::new(Mutex::new(HashMap::from([(
|
let physical_properties = output.physical_properties();
|
||||||
|
let ipc_outputs = Rc::new(RefCell::new(HashMap::from([(
|
||||||
|
"winit".to_owned(),
|
||||||
|
niri_ipc::Output {
|
||||||
|
name: output.name(),
|
||||||
|
make: physical_properties.make,
|
||||||
|
model: physical_properties.model,
|
||||||
|
physical_size: None,
|
||||||
|
modes: vec![niri_ipc::Mode {
|
||||||
|
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
|
||||||
|
height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16,
|
||||||
|
refresh_rate: 60_000,
|
||||||
|
}],
|
||||||
|
current_mode: Some(0),
|
||||||
|
},
|
||||||
|
)])));
|
||||||
|
|
||||||
|
let enabled_outputs = Arc::new(Mutex::new(HashMap::from([(
|
||||||
"winit".to_owned(),
|
"winit".to_owned(),
|
||||||
output.clone(),
|
output.clone(),
|
||||||
)])));
|
)])));
|
||||||
@@ -89,6 +94,12 @@ impl Winit {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut ipc_outputs = winit.ipc_outputs.borrow_mut();
|
||||||
|
let mode = &mut ipc_outputs.get_mut("winit").unwrap().modes[0];
|
||||||
|
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
|
||||||
|
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
|
||||||
|
|
||||||
state.niri.output_resized(winit.output.clone());
|
state.niri.output_resized(winit.output.clone());
|
||||||
}
|
}
|
||||||
WinitEvent::Input(event) => state.process_input_event(event),
|
WinitEvent::Input(event) => state.process_input_event(event),
|
||||||
@@ -108,22 +119,20 @@ impl Winit {
|
|||||||
output,
|
output,
|
||||||
backend,
|
backend,
|
||||||
damage_tracker,
|
damage_tracker,
|
||||||
connectors,
|
ipc_outputs,
|
||||||
|
enabled_outputs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&mut self, niri: &mut Niri) {
|
pub fn init(&mut self, niri: &mut Niri) {
|
||||||
// For some reason, binding the display here causes damage tracker artifacts.
|
if let Err(err) = self
|
||||||
//
|
.backend
|
||||||
// use smithay::backend::renderer::ImportEgl;
|
.renderer()
|
||||||
//
|
.bind_wl_display(&niri.display_handle)
|
||||||
// if let Err(err) = self
|
{
|
||||||
// .backend
|
warn!("error binding renderer wl_display: {err}");
|
||||||
// .renderer()
|
}
|
||||||
// .bind_wl_display(&niri.display_handle)
|
|
||||||
// {
|
|
||||||
// warn!("error binding renderer wl_display: {err}");
|
|
||||||
// }
|
|
||||||
niri.add_output(self.output.clone(), None);
|
niri.add_output(self.output.clone(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,23 +140,25 @@ impl Winit {
|
|||||||
"winit".to_owned()
|
"winit".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn renderer(&mut self) -> &mut GlesRenderer {
|
pub fn with_primary_renderer<T>(
|
||||||
self.backend.renderer()
|
&mut self,
|
||||||
|
f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
Some(f(self.backend.renderer()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
|
||||||
&mut self,
|
|
||||||
niri: &mut Niri,
|
|
||||||
output: &Output,
|
|
||||||
elements: &[OutputRenderElements<GlesRenderer>],
|
|
||||||
) -> RenderResult {
|
|
||||||
let _span = tracy_client::span!("Winit::render");
|
let _span = tracy_client::span!("Winit::render");
|
||||||
|
|
||||||
|
// Render the elements.
|
||||||
|
let elements = niri.render::<GlesRenderer>(self.backend.renderer(), output, true);
|
||||||
|
|
||||||
|
// Hand them over to winit.
|
||||||
self.backend.bind().unwrap();
|
self.backend.bind().unwrap();
|
||||||
let age = self.backend.buffer_age().unwrap();
|
let age = self.backend.buffer_age().unwrap();
|
||||||
let res = self
|
let res = self
|
||||||
.damage_tracker
|
.damage_tracker
|
||||||
.render_output(self.backend.renderer(), age, elements, [0.; 4])
|
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
niri.update_primary_scanout_output(output, &res.states);
|
niri.update_primary_scanout_output(output, &res.states);
|
||||||
@@ -202,7 +213,21 @@ impl Winit {
|
|||||||
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
|
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
||||||
self.connectors.clone()
|
match self.backend.renderer().import_dmabuf(dmabuf, None) {
|
||||||
|
Ok(_texture) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
debug!("error importing dmabuf: {err:?}");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
|
||||||
|
self.ipc_outputs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
||||||
|
self.enabled_outputs.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use pangocairo::cairo::{self, ImageSurface};
|
||||||
|
use pangocairo::pango::FontDescription;
|
||||||
|
use smithay::backend::renderer::element::memory::{
|
||||||
|
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||||
|
use smithay::backend::renderer::element::{Element, Kind};
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::gbm::Format as Fourcc;
|
||||||
|
use smithay::utils::Transform;
|
||||||
|
|
||||||
|
use crate::animation::Animation;
|
||||||
|
use crate::render_helpers::NiriRenderer;
|
||||||
|
|
||||||
|
const TEXT: &str = "Failed to parse the config file. \
|
||||||
|
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
|
||||||
|
to see the errors.";
|
||||||
|
const PADDING: i32 = 8;
|
||||||
|
const FONT: &str = "sans 14px";
|
||||||
|
const BORDER: i32 = 4;
|
||||||
|
|
||||||
|
pub struct ConfigErrorNotification {
|
||||||
|
state: State,
|
||||||
|
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Hidden,
|
||||||
|
Showing(Animation),
|
||||||
|
Shown(Duration),
|
||||||
|
Hiding(Animation),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ConfigErrorNotificationRenderElement<R> =
|
||||||
|
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
|
||||||
|
|
||||||
|
impl ConfigErrorNotification {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: State::Hidden,
|
||||||
|
buffers: RefCell::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self) {
|
||||||
|
// Show from scratch even if already showing to bring attention.
|
||||||
|
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide(&mut self) {
|
||||||
|
if matches!(self.state, State::Hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = State::Hiding(Animation::new(1., 0., Duration::from_millis(250)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
|
||||||
|
match &mut self.state {
|
||||||
|
State::Hidden => (),
|
||||||
|
State::Showing(anim) => {
|
||||||
|
anim.set_current_time(target_presentation_time);
|
||||||
|
if anim.is_done() {
|
||||||
|
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Shown(deadline) => {
|
||||||
|
if target_presentation_time >= *deadline {
|
||||||
|
self.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Hiding(anim) => {
|
||||||
|
anim.set_current_time(target_presentation_time);
|
||||||
|
if anim.is_done() {
|
||||||
|
self.state = State::Hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_animations_ongoing(&self) -> bool {
|
||||||
|
!matches!(self.state, State::Hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
output: &Output,
|
||||||
|
) -> Option<ConfigErrorNotificationRenderElement<R>> {
|
||||||
|
if matches!(self.state, State::Hidden) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = output.current_scale().integer_scale();
|
||||||
|
|
||||||
|
let mut buffers = self.buffers.borrow_mut();
|
||||||
|
let buffer = buffers
|
||||||
|
.entry(scale)
|
||||||
|
.or_insert_with_key(move |&scale| render(scale).ok());
|
||||||
|
let buffer = buffer.as_ref()?;
|
||||||
|
|
||||||
|
let elem = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
|
renderer,
|
||||||
|
(0., 0.),
|
||||||
|
buffer,
|
||||||
|
Some(0.9),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let output_transform = output.current_transform();
|
||||||
|
let output_mode = output.current_mode().unwrap();
|
||||||
|
let output_size = output_transform.transform_size(output_mode.size);
|
||||||
|
|
||||||
|
let buffer_size = elem
|
||||||
|
.geometry(output.current_scale().fractional_scale().into())
|
||||||
|
.size;
|
||||||
|
|
||||||
|
let y_range = buffer_size.h + PADDING * 2 * scale;
|
||||||
|
|
||||||
|
let x = (output_size.w / 2 - buffer_size.w / 2).max(0);
|
||||||
|
let y = match &self.state {
|
||||||
|
State::Hidden => unreachable!(),
|
||||||
|
State::Showing(anim) | State::Hiding(anim) => {
|
||||||
|
(-buffer_size.h as f64 + anim.value() * y_range as f64).round() as i32
|
||||||
|
}
|
||||||
|
State::Shown(_) => PADDING * 2 * scale,
|
||||||
|
};
|
||||||
|
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
|
||||||
|
|
||||||
|
Some(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||||
|
let _span = tracy_client::span!("config_error_notification::render");
|
||||||
|
|
||||||
|
let padding = PADDING * scale;
|
||||||
|
|
||||||
|
let mut font = FontDescription::from_string(FONT);
|
||||||
|
font.set_absolute_size((font.size() * scale).into());
|
||||||
|
|
||||||
|
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||||
|
let cr = cairo::Context::new(&surface)?;
|
||||||
|
let layout = pangocairo::create_layout(&cr);
|
||||||
|
layout.set_font_description(Some(&font));
|
||||||
|
layout.set_markup(TEXT);
|
||||||
|
|
||||||
|
let (mut width, mut height) = layout.pixel_size();
|
||||||
|
width += padding * 2;
|
||||||
|
height += padding * 2;
|
||||||
|
|
||||||
|
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
|
||||||
|
width = (width + scale - 1) / scale * scale;
|
||||||
|
height = (height + scale - 1) / scale * scale;
|
||||||
|
|
||||||
|
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
|
||||||
|
let cr = cairo::Context::new(&surface)?;
|
||||||
|
cr.set_source_rgb(0.1, 0.1, 0.1);
|
||||||
|
cr.paint()?;
|
||||||
|
|
||||||
|
cr.move_to(padding.into(), padding.into());
|
||||||
|
let layout = pangocairo::create_layout(&cr);
|
||||||
|
layout.set_font_description(Some(&font));
|
||||||
|
layout.set_markup(TEXT);
|
||||||
|
|
||||||
|
cr.set_source_rgb(1., 1., 1.);
|
||||||
|
pangocairo::show_layout(&cr, &layout);
|
||||||
|
|
||||||
|
cr.move_to(0., 0.);
|
||||||
|
cr.line_to(width.into(), 0.);
|
||||||
|
cr.line_to(width.into(), height.into());
|
||||||
|
cr.line_to(0., height.into());
|
||||||
|
cr.line_to(0., 0.);
|
||||||
|
cr.set_source_rgb(1., 0.3, 0.3);
|
||||||
|
cr.set_line_width((BORDER * scale).into());
|
||||||
|
cr.stroke()?;
|
||||||
|
drop(cr);
|
||||||
|
|
||||||
|
let data = surface.take_data().unwrap();
|
||||||
|
let buffer = MemoryRenderBuffer::from_slice(
|
||||||
|
&data,
|
||||||
|
Fourcc::Argb8888,
|
||||||
|
(width, height),
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
+5
-12
@@ -8,8 +8,7 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::renderer::element::texture::TextureBuffer;
|
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
|
||||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||||
@@ -224,7 +223,7 @@ pub enum RenderCursor {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<TextureBuffer<GlesTexture>>>;
|
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CursorTextureCache {
|
pub struct CursorTextureCache {
|
||||||
@@ -238,12 +237,11 @@ impl CursorTextureCache {
|
|||||||
|
|
||||||
pub fn get(
|
pub fn get(
|
||||||
&self,
|
&self,
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
icon: CursorIcon,
|
icon: CursorIcon,
|
||||||
scale: i32,
|
scale: i32,
|
||||||
cursor: &XCursor,
|
cursor: &XCursor,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> TextureBuffer<GlesTexture> {
|
) -> MemoryRenderBuffer {
|
||||||
self.cache
|
self.cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.entry((icon, scale))
|
.entry((icon, scale))
|
||||||
@@ -252,19 +250,14 @@ impl CursorTextureCache {
|
|||||||
.frames()
|
.frames()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|frame| {
|
.map(|frame| {
|
||||||
let _span = tracy_client::span!("create TextureBuffer");
|
MemoryRenderBuffer::from_slice(
|
||||||
|
|
||||||
TextureBuffer::from_memory(
|
|
||||||
renderer,
|
|
||||||
&frame.pixels_rgba,
|
&frame.pixels_rgba,
|
||||||
Fourcc::Abgr8888,
|
Fourcc::Argb8888,
|
||||||
(frame.width as i32, frame.height as i32),
|
(frame.width as i32, frame.height as i32),
|
||||||
false,
|
|
||||||
scale,
|
scale,
|
||||||
Transform::Normal,
|
Transform::Normal,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.unwrap()
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})[idx]
|
})[idx]
|
||||||
|
|||||||
+2
-2
@@ -45,7 +45,7 @@ impl DBusServers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
||||||
let display_config = DisplayConfig::new(backend.connectors());
|
let display_config = DisplayConfig::new(backend.enabled_outputs());
|
||||||
dbus.conn_display_config = try_start(display_config);
|
dbus.conn_display_config = try_start(display_config);
|
||||||
|
|
||||||
let (to_niri, from_screenshot) = calloop::channel::channel();
|
let (to_niri, from_screenshot) = calloop::channel::channel();
|
||||||
@@ -75,7 +75,7 @@ impl DBusServers {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let screen_cast = ScreenCast::new(backend.connectors(), to_niri);
|
let screen_cast = ScreenCast::new(backend.enabled_outputs(), to_niri);
|
||||||
dbus.conn_screen_cast = try_start(screen_cast);
|
dbus.conn_screen_cast = try_start(screen_cast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ use std::sync::{Arc, Mutex};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use zbus::fdo::RequestNameFlags;
|
use zbus::fdo::RequestNameFlags;
|
||||||
use zbus::zvariant::{OwnedValue, Type};
|
use zbus::zvariant::{self, OwnedValue, Type};
|
||||||
use zbus::{dbus_interface, fdo};
|
use zbus::{dbus_interface, fdo, SignalContext};
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
|
|
||||||
pub struct DisplayConfig {
|
pub struct DisplayConfig {
|
||||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Type)]
|
#[derive(Serialize, Type)]
|
||||||
@@ -53,18 +53,49 @@ impl DisplayConfig {
|
|||||||
HashMap<String, OwnedValue>,
|
HashMap<String, OwnedValue>,
|
||||||
)> {
|
)> {
|
||||||
// Construct the DBus response.
|
// Construct the DBus response.
|
||||||
let monitors: Vec<Monitor> = self
|
let mut monitors: Vec<Monitor> = self
|
||||||
.connectors
|
.enabled_outputs
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.keys()
|
.keys()
|
||||||
.map(|c| Monitor {
|
.map(|c| {
|
||||||
names: (c.clone(), String::new(), String::new(), String::new()),
|
// Loosely matches the check in Mutter.
|
||||||
modes: vec![],
|
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||||
properties: HashMap::new(),
|
|
||||||
|
// FIXME: use proper serial when we have libdisplay-info.
|
||||||
|
// A serial is required for correct session restore by xdp-gnome.
|
||||||
|
let serial = c.clone();
|
||||||
|
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
if is_laptop_panel {
|
||||||
|
properties.insert(
|
||||||
|
String::from("display-name"),
|
||||||
|
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
properties.insert(
|
||||||
|
String::from("is-builtin"),
|
||||||
|
OwnedValue::from(is_laptop_panel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Monitor {
|
||||||
|
names: (c.clone(), String::new(), String::new(), serial),
|
||||||
|
modes: vec![],
|
||||||
|
properties,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Sort the built-in monitor first, then by connector name.
|
||||||
|
monitors.sort_unstable_by(|a, b| {
|
||||||
|
let a_is_builtin = a.properties.contains_key("display-name");
|
||||||
|
let b_is_builtin = b.properties.contains_key("display-name");
|
||||||
|
a_is_builtin
|
||||||
|
.cmp(&b_is_builtin)
|
||||||
|
.reverse()
|
||||||
|
.then_with(|| a.names.0.cmp(&b.names.0))
|
||||||
|
});
|
||||||
|
|
||||||
let logical_monitors = monitors
|
let logical_monitors = monitors
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| LogicalMonitor {
|
.map(|m| LogicalMonitor {
|
||||||
@@ -81,12 +112,13 @@ impl DisplayConfig {
|
|||||||
Ok((0, monitors, logical_monitors, HashMap::new()))
|
Ok((0, monitors, logical_monitors, HashMap::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: monitors-changed signal.
|
#[dbus_interface(signal)]
|
||||||
|
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayConfig {
|
impl DisplayConfig {
|
||||||
pub fn new(connectors: Arc<Mutex<HashMap<String, Output>>>) -> Self {
|
pub fn new(enabled_outputs: Arc<Mutex<HashMap<String, Output>>>) -> Self {
|
||||||
Self { connectors }
|
Self { enabled_outputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,26 @@ use serde::Deserialize;
|
|||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::reexports::calloop;
|
use smithay::reexports::calloop;
|
||||||
use zbus::fdo::RequestNameFlags;
|
use zbus::fdo::RequestNameFlags;
|
||||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
|
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
||||||
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
|
use crate::utils::output_size;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ScreenCast {
|
pub struct ScreenCast {
|
||||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
|
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
id: usize,
|
id: usize,
|
||||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
|
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +55,13 @@ pub struct Stream {
|
|||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SerializeDict, Type, Value)]
|
||||||
|
#[zvariant(signature = "dict")]
|
||||||
|
struct StreamParameters {
|
||||||
|
/// Size of the stream in logical coordinates.
|
||||||
|
size: (i32, i32),
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ScreenCastToNiri {
|
pub enum ScreenCastToNiri {
|
||||||
StartCast {
|
StartCast {
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
@@ -62,6 +72,7 @@ pub enum ScreenCastToNiri {
|
|||||||
StopCast {
|
StopCast {
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
},
|
},
|
||||||
|
Redraw(Output),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
|
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||||
@@ -82,7 +93,11 @@ impl ScreenCast {
|
|||||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
|
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
|
||||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||||
|
|
||||||
let session = Session::new(session_id, self.connectors.clone(), self.to_niri.clone());
|
let session = Session::new(
|
||||||
|
session_id,
|
||||||
|
self.enabled_outputs.clone(),
|
||||||
|
self.to_niri.clone(),
|
||||||
|
);
|
||||||
match server.at(&path, session.clone()).await {
|
match server.at(&path, session.clone()).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
let iface = server.interface(&path).await.unwrap();
|
let iface = server.interface(&path).await.unwrap();
|
||||||
@@ -149,7 +164,7 @@ impl Session {
|
|||||||
) -> fdo::Result<OwnedObjectPath> {
|
) -> fdo::Result<OwnedObjectPath> {
|
||||||
debug!(connector, ?properties, "record_monitor");
|
debug!(connector, ?properties, "record_monitor");
|
||||||
|
|
||||||
let Some(output) = self.connectors.lock().unwrap().get(connector).cloned() else {
|
let Some(output) = self.enabled_outputs.lock().unwrap().get(connector).cloned() else {
|
||||||
return Err(fdo::Error::Failed("no such monitor".to_owned()));
|
return Err(fdo::Error::Failed("no such monitor".to_owned()));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,15 +203,21 @@ impl Stream {
|
|||||||
#[dbus_interface(signal)]
|
#[dbus_interface(signal)]
|
||||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
||||||
-> zbus::Result<()>;
|
-> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[dbus_interface(property)]
|
||||||
|
async fn parameters(&self) -> StreamParameters {
|
||||||
|
let size = output_size(&self.output).into();
|
||||||
|
StreamParameters { size }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScreenCast {
|
impl ScreenCast {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
connectors,
|
enabled_outputs,
|
||||||
to_niri,
|
to_niri,
|
||||||
sessions: Arc::new(Mutex::new(vec![])),
|
sessions: Arc::new(Mutex::new(vec![])),
|
||||||
}
|
}
|
||||||
@@ -221,12 +242,12 @@ impl Start for ScreenCast {
|
|||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: usize,
|
id: usize,
|
||||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
connectors,
|
enabled_outputs,
|
||||||
streams: Arc::new(Mutex::new(vec![])),
|
streams: Arc::new(Mutex::new(vec![])),
|
||||||
to_niri,
|
to_niri,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,13 @@ impl ServiceChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (sock1, sock2) = UnixStream::pair().unwrap();
|
let (sock1, sock2) = UnixStream::pair().unwrap();
|
||||||
self.display
|
let data = Arc::new(ClientState {
|
||||||
.insert_client(sock2, Arc::new(ClientState::default()))
|
compositor_state: Default::default(),
|
||||||
.unwrap();
|
// Would be nice to thread config here but for now it's fine.
|
||||||
|
can_view_decoration_globals: false,
|
||||||
|
restricted: false,
|
||||||
|
});
|
||||||
|
self.display.insert_client(sock2, data).unwrap();
|
||||||
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
|
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use pangocairo::cairo::{self, ImageSurface};
|
||||||
|
use pangocairo::pango::{Alignment, FontDescription};
|
||||||
|
use smithay::backend::renderer::element::memory::{
|
||||||
|
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||||
|
use smithay::backend::renderer::element::{Element, Kind};
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::gbm::Format as Fourcc;
|
||||||
|
use smithay::utils::Transform;
|
||||||
|
|
||||||
|
use crate::render_helpers::NiriRenderer;
|
||||||
|
|
||||||
|
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
|
||||||
|
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
|
||||||
|
const PADDING: i32 = 16;
|
||||||
|
const FONT: &str = "sans 14px";
|
||||||
|
const BORDER: i32 = 8;
|
||||||
|
|
||||||
|
pub struct ExitConfirmDialog {
|
||||||
|
is_open: bool,
|
||||||
|
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ExitConfirmDialogRenderElement<R> =
|
||||||
|
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
|
||||||
|
|
||||||
|
impl ExitConfirmDialog {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
is_open: false,
|
||||||
|
buffers: RefCell::new(HashMap::from([(1, Some(render(1)?))])),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self) -> bool {
|
||||||
|
if !self.is_open {
|
||||||
|
self.is_open = true;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide(&mut self) -> bool {
|
||||||
|
if self.is_open {
|
||||||
|
self.is_open = false;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.is_open
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
output: &Output,
|
||||||
|
) -> Option<ExitConfirmDialogRenderElement<R>> {
|
||||||
|
if !self.is_open {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = output.current_scale().integer_scale();
|
||||||
|
|
||||||
|
let mut buffers = self.buffers.borrow_mut();
|
||||||
|
let fallback = buffers[&1].clone().unwrap();
|
||||||
|
let buffer = buffers.entry(scale).or_insert_with(|| render(scale).ok());
|
||||||
|
let buffer = buffer.as_ref().unwrap_or(&fallback);
|
||||||
|
|
||||||
|
let elem = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
|
renderer,
|
||||||
|
(0., 0.),
|
||||||
|
buffer,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let output_transform = output.current_transform();
|
||||||
|
let output_mode = output.current_mode().unwrap();
|
||||||
|
let output_size = output_transform.transform_size(output_mode.size);
|
||||||
|
|
||||||
|
let buffer_size = elem
|
||||||
|
.geometry(output.current_scale().fractional_scale().into())
|
||||||
|
.size;
|
||||||
|
|
||||||
|
let x = (output_size.w / 2 - buffer_size.w / 2).max(0);
|
||||||
|
let y = (output_size.h / 2 - buffer_size.h / 2).max(0);
|
||||||
|
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
|
||||||
|
|
||||||
|
Some(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
||||||
|
let _span = tracy_client::span!("exit_confirm_dialog::render");
|
||||||
|
|
||||||
|
let padding = PADDING * scale;
|
||||||
|
|
||||||
|
let mut font = FontDescription::from_string(FONT);
|
||||||
|
font.set_absolute_size((font.size() * scale).into());
|
||||||
|
|
||||||
|
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||||
|
let cr = cairo::Context::new(&surface)?;
|
||||||
|
let layout = pangocairo::create_layout(&cr);
|
||||||
|
layout.set_font_description(Some(&font));
|
||||||
|
layout.set_alignment(Alignment::Center);
|
||||||
|
layout.set_markup(TEXT);
|
||||||
|
|
||||||
|
let (mut width, mut height) = layout.pixel_size();
|
||||||
|
width += padding * 2;
|
||||||
|
height += padding * 2;
|
||||||
|
|
||||||
|
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
|
||||||
|
width = (width + scale - 1) / scale * scale;
|
||||||
|
height = (height + scale - 1) / scale * scale;
|
||||||
|
|
||||||
|
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
|
||||||
|
let cr = cairo::Context::new(&surface)?;
|
||||||
|
cr.set_source_rgb(0.1, 0.1, 0.1);
|
||||||
|
cr.paint()?;
|
||||||
|
|
||||||
|
cr.move_to(padding.into(), padding.into());
|
||||||
|
let layout = pangocairo::create_layout(&cr);
|
||||||
|
layout.set_font_description(Some(&font));
|
||||||
|
layout.set_alignment(Alignment::Center);
|
||||||
|
layout.set_markup(TEXT);
|
||||||
|
|
||||||
|
cr.set_source_rgb(1., 1., 1.);
|
||||||
|
pangocairo::show_layout(&cr, &layout);
|
||||||
|
|
||||||
|
cr.move_to(0., 0.);
|
||||||
|
cr.line_to(width.into(), 0.);
|
||||||
|
cr.line_to(width.into(), height.into());
|
||||||
|
cr.line_to(0., height.into());
|
||||||
|
cr.line_to(0., 0.);
|
||||||
|
cr.set_source_rgb(1., 0.3, 0.3);
|
||||||
|
cr.set_line_width((BORDER * scale).into());
|
||||||
|
cr.stroke()?;
|
||||||
|
drop(cr);
|
||||||
|
|
||||||
|
let data = surface.take_data().unwrap();
|
||||||
|
let buffer = MemoryRenderBuffer::from_slice(
|
||||||
|
&data,
|
||||||
|
Fourcc::Argb8888,
|
||||||
|
(width, height),
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
+18
-15
@@ -1,7 +1,6 @@
|
|||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||||
use smithay::desktop::find_popup_root_surface;
|
|
||||||
use smithay::input::pointer::CursorImageStatus;
|
use smithay::input::pointer::CursorImageStatus;
|
||||||
use smithay::reexports::calloop::Interest;
|
use smithay::reexports::calloop::Interest;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||||
@@ -19,6 +18,7 @@ use smithay::{delegate_compositor, delegate_shm};
|
|||||||
|
|
||||||
use super::xdg_shell;
|
use super::xdg_shell;
|
||||||
use crate::niri::{ClientState, State};
|
use crate::niri::{ClientState, State};
|
||||||
|
use crate::utils::clone2;
|
||||||
|
|
||||||
impl CompositorHandler for State {
|
impl CompositorHandler for State {
|
||||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||||
@@ -30,7 +30,12 @@ impl CompositorHandler for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_subsurface(&mut self, surface: &WlSurface, parent: &WlSurface) {
|
fn new_subsurface(&mut self, surface: &WlSurface, parent: &WlSurface) {
|
||||||
if let Some((_, output)) = self.niri.layout.find_window_and_output(parent) {
|
let mut root = parent.clone();
|
||||||
|
while let Some(parent) = get_parent(&root) {
|
||||||
|
root = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(output) = self.niri.output_for_root(&root) {
|
||||||
let scale = output.current_scale().integer_scale();
|
let scale = output.current_scale().integer_scale();
|
||||||
let transform = output.current_transform();
|
let transform = output.current_transform();
|
||||||
with_states(surface, |data| {
|
with_states(surface, |data| {
|
||||||
@@ -77,6 +82,7 @@ impl CompositorHandler for State {
|
|||||||
let _span = tracy_client::span!("CompositorHandler::commit");
|
let _span = tracy_client::span!("CompositorHandler::commit");
|
||||||
|
|
||||||
on_commit_buffer_handler::<Self>(surface);
|
on_commit_buffer_handler::<Self>(surface);
|
||||||
|
self.backend.early_import(surface);
|
||||||
|
|
||||||
if is_sync_subsurface(surface) {
|
if is_sync_subsurface(surface) {
|
||||||
return;
|
return;
|
||||||
@@ -98,11 +104,7 @@ impl CompositorHandler for State {
|
|||||||
let window = entry.remove();
|
let window = entry.remove();
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
|
|
||||||
if let Some(output) = self
|
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
|
||||||
.niri
|
|
||||||
.layout
|
|
||||||
.add_window(window, true, None, false)
|
|
||||||
.cloned()
|
|
||||||
{
|
{
|
||||||
self.niri.queue_redraw(output);
|
self.niri.queue_redraw(output);
|
||||||
}
|
}
|
||||||
@@ -116,8 +118,9 @@ impl CompositorHandler for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is a commit of a previously-mapped root or a non-toplevel root.
|
// This is a commit of a previously-mapped root or a non-toplevel root.
|
||||||
if let Some((window, output)) = self.niri.layout.find_window_and_output(surface) {
|
if let Some(win_out) = self.niri.layout.find_window_and_output(surface) {
|
||||||
// This is a commit of a previously-mapped toplevel.
|
let (window, output) = clone2(win_out);
|
||||||
|
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
|
|
||||||
// This is a commit of a previously-mapped toplevel.
|
// This is a commit of a previously-mapped toplevel.
|
||||||
@@ -135,6 +138,9 @@ impl CompositorHandler for State {
|
|||||||
// The toplevel remains mapped.
|
// The toplevel remains mapped.
|
||||||
self.niri.layout.update_window(&window);
|
self.niri.layout.update_window(&window);
|
||||||
|
|
||||||
|
// Popup placement depends on window size which might have changed.
|
||||||
|
self.update_reactive_popups(&window, &output);
|
||||||
|
|
||||||
self.niri.queue_redraw(output);
|
self.niri.queue_redraw(output);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,7 +150,7 @@ impl CompositorHandler for State {
|
|||||||
|
|
||||||
// This is a commit of a non-root or a non-toplevel root.
|
// This is a commit of a non-root or a non-toplevel root.
|
||||||
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
||||||
if let Some((window, output)) = root_window_output {
|
if let Some((window, output)) = root_window_output.map(clone2) {
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
self.niri.layout.update_window(&window);
|
self.niri.layout.update_window(&window);
|
||||||
self.niri.queue_redraw(output);
|
self.niri.queue_redraw(output);
|
||||||
@@ -154,11 +160,8 @@ impl CompositorHandler for State {
|
|||||||
// This might be a popup.
|
// This might be a popup.
|
||||||
self.popups_handle_commit(surface);
|
self.popups_handle_commit(surface);
|
||||||
if let Some(popup) = self.niri.popups.find_popup(surface) {
|
if let Some(popup) = self.niri.popups.find_popup(surface) {
|
||||||
if let Ok(root) = find_popup_root_surface(&popup) {
|
if let Some(output) = self.output_for_popup(&popup) {
|
||||||
let root_window_output = self.niri.layout.find_window_and_output(&root);
|
self.niri.queue_redraw(output.clone());
|
||||||
if let Some((_window, output)) = root_window_output {
|
|
||||||
self.niri.queue_redraw(output);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use smithay::wayland::shell::wlr_layer::{
|
|||||||
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||||
WlrLayerShellState,
|
WlrLayerShellState,
|
||||||
};
|
};
|
||||||
|
use smithay::wayland::shell::xdg::PopupSurface;
|
||||||
|
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
|
|
||||||
@@ -52,6 +53,10 @@ impl WlrLayerShellHandler for State {
|
|||||||
self.niri.output_resized(output);
|
self.niri.output_resized(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
|
||||||
|
self.unconstrain_popup(&popup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delegate_layer_shell!(State);
|
delegate_layer_shell!(State);
|
||||||
|
|
||||||
|
|||||||
+51
-19
@@ -9,9 +9,8 @@ use std::sync::Arc;
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
use smithay::backend::renderer::ImportDma;
|
|
||||||
use smithay::desktop::{PopupKind, PopupManager};
|
use smithay::desktop::{PopupKind, PopupManager};
|
||||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus};
|
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||||
@@ -22,6 +21,10 @@ use smithay::utils::{Logical, Rectangle, Size};
|
|||||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||||
|
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||||
|
use smithay::wayland::security_context::{
|
||||||
|
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||||
|
};
|
||||||
use smithay::wayland::selection::data_device::{
|
use smithay::wayland::selection::data_device::{
|
||||||
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
||||||
ServerDndGrabHandler,
|
ServerDndGrabHandler,
|
||||||
@@ -36,13 +39,14 @@ use smithay::wayland::session_lock::{
|
|||||||
};
|
};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||||
delegate_input_method_manager, delegate_output, delegate_pointer_gestures,
|
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
|
||||||
delegate_presentation, delegate_primary_selection, delegate_seat, delegate_session_lock,
|
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||||
|
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||||
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::layout::output_size;
|
use crate::niri::{ClientState, State};
|
||||||
use crate::niri::State;
|
use crate::utils::output_size;
|
||||||
|
|
||||||
impl SeatHandler for State {
|
impl SeatHandler for State {
|
||||||
type KeyboardFocus = WlSurface;
|
type KeyboardFocus = WlSurface;
|
||||||
@@ -74,14 +78,23 @@ delegate_seat!(State);
|
|||||||
delegate_cursor_shape!(State);
|
delegate_cursor_shape!(State);
|
||||||
delegate_tablet_manager!(State);
|
delegate_tablet_manager!(State);
|
||||||
delegate_pointer_gestures!(State);
|
delegate_pointer_gestures!(State);
|
||||||
|
delegate_relative_pointer!(State);
|
||||||
delegate_text_input_manager!(State);
|
delegate_text_input_manager!(State);
|
||||||
|
|
||||||
|
impl PointerConstraintsHandler for State {
|
||||||
|
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||||
|
self.niri.maybe_activate_pointer_constraint(
|
||||||
|
pointer.current_location(),
|
||||||
|
&self.niri.pointer_focus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_pointer_constraints!(State);
|
||||||
|
|
||||||
impl InputMethodHandler for State {
|
impl InputMethodHandler for State {
|
||||||
fn new_popup(&mut self, surface: PopupSurface) {
|
fn new_popup(&mut self, surface: PopupSurface) {
|
||||||
if let Some((_, output)) = surface
|
let popup = PopupKind::from(surface.clone());
|
||||||
.get_parent()
|
if let Some(output) = self.output_for_popup(&popup) {
|
||||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent.surface))
|
|
||||||
{
|
|
||||||
let scale = output.current_scale().integer_scale();
|
let scale = output.current_scale().integer_scale();
|
||||||
let transform = output.current_transform();
|
let transform = output.current_transform();
|
||||||
let wl_surface = surface.wl_surface();
|
let wl_surface = surface.wl_surface();
|
||||||
@@ -89,7 +102,7 @@ impl InputMethodHandler for State {
|
|||||||
send_surface_state(wl_surface, data, scale, transform);
|
send_surface_state(wl_surface, data, scale, transform);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Err(err) = self.niri.popups.track_popup(PopupKind::from(surface)) {
|
if let Err(err) = self.niri.popups.track_popup(popup) {
|
||||||
warn!("error tracking ime popup {err:?}");
|
warn!("error tracking ime popup {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +195,7 @@ delegate_presentation!(State);
|
|||||||
|
|
||||||
impl DmabufHandler for State {
|
impl DmabufHandler for State {
|
||||||
fn dmabuf_state(&mut self) -> &mut DmabufState {
|
fn dmabuf_state(&mut self) -> &mut DmabufState {
|
||||||
self.backend.tty().dmabuf_state()
|
&mut self.niri.dmabuf_state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dmabuf_imported(
|
fn dmabuf_imported(
|
||||||
@@ -191,15 +204,11 @@ impl DmabufHandler for State {
|
|||||||
dmabuf: Dmabuf,
|
dmabuf: Dmabuf,
|
||||||
notifier: ImportNotifier,
|
notifier: ImportNotifier,
|
||||||
) {
|
) {
|
||||||
let renderer = self.backend.renderer().expect(
|
match self.backend.import_dmabuf(&dmabuf) {
|
||||||
"the dmabuf global must be created and destroyed together with the output device",
|
Ok(_) => {
|
||||||
);
|
|
||||||
match renderer.import_dmabuf(&dmabuf, None) {
|
|
||||||
Ok(_texture) => {
|
|
||||||
let _ = notifier.successful::<State>();
|
let _ = notifier.successful::<State>();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(_) => {
|
||||||
debug!("error importing dmabuf: {err:?}");
|
|
||||||
notifier.failed();
|
notifier.failed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,3 +254,26 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
|
|||||||
});
|
});
|
||||||
surface.send_configure();
|
surface.send_configure();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SecurityContextHandler for State {
|
||||||
|
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
|
||||||
|
self.niri
|
||||||
|
.event_loop
|
||||||
|
.insert_source(source, move |client, _, state| {
|
||||||
|
let config = state.niri.config.borrow();
|
||||||
|
let data = Arc::new(ClientState {
|
||||||
|
compositor_state: Default::default(),
|
||||||
|
can_view_decoration_globals: config.prefer_no_csd,
|
||||||
|
restricted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||||
|
error!("error inserting client: {err}");
|
||||||
|
} else {
|
||||||
|
trace!("inserted a new restricted client, context={context:?}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_security_context!(State);
|
||||||
|
|||||||
+266
-41
@@ -1,13 +1,21 @@
|
|||||||
use smithay::desktop::{find_popup_root_surface, PopupKind, Window};
|
use smithay::desktop::{
|
||||||
|
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
|
||||||
|
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
|
||||||
|
WindowSurfaceType,
|
||||||
|
};
|
||||||
|
use smithay::input::pointer::Focus;
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
|
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
|
||||||
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
|
||||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge};
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_output;
|
use smithay::reexports::wayland_server::protocol::wl_output;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::utils::Serial;
|
use smithay::utils::{Logical, Rectangle, Serial};
|
||||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||||
|
use smithay::wayland::input_method::InputMethodSeat;
|
||||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||||
|
use smithay::wayland::shell::wlr_layer::Layer;
|
||||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||||
use smithay::wayland::shell::xdg::{
|
use smithay::wayland::shell::xdg::{
|
||||||
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
|
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
|
||||||
@@ -15,7 +23,8 @@ use smithay::wayland::shell::xdg::{
|
|||||||
};
|
};
|
||||||
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
|
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
|
||||||
|
|
||||||
use crate::niri::State;
|
use crate::niri::{PopupGrabState, State};
|
||||||
|
use crate::utils::clone2;
|
||||||
|
|
||||||
impl XdgShellHandler for State {
|
impl XdgShellHandler for State {
|
||||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||||
@@ -49,8 +58,8 @@ impl XdgShellHandler for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
|
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
|
||||||
// FIXME: adjust the geometry so the popup doesn't overflow at least off the top and bottom
|
self.unconstrain_popup(&surface);
|
||||||
// screen edges, and ideally off the view size.
|
|
||||||
if let Err(err) = self.niri.popups.track_popup(PopupKind::Xdg(surface)) {
|
if let Err(err) = self.niri.popups.track_popup(PopupKind::Xdg(surface)) {
|
||||||
warn!("error tracking popup: {err:?}");
|
warn!("error tracking popup: {err:?}");
|
||||||
}
|
}
|
||||||
@@ -76,18 +85,111 @@ impl XdgShellHandler for State {
|
|||||||
positioner: PositionerState,
|
positioner: PositionerState,
|
||||||
token: u32,
|
token: u32,
|
||||||
) {
|
) {
|
||||||
// FIXME: adjust the geometry so the popup doesn't overflow at least off the top and bottom
|
|
||||||
// screen edges, and ideally off the view size.
|
|
||||||
surface.with_pending_state(|state| {
|
surface.with_pending_state(|state| {
|
||||||
let geometry = positioner.get_geometry();
|
let geometry = positioner.get_geometry();
|
||||||
state.geometry = geometry;
|
state.geometry = geometry;
|
||||||
state.positioner = positioner;
|
state.positioner = positioner;
|
||||||
});
|
});
|
||||||
|
self.unconstrain_popup(&surface);
|
||||||
surface.send_repositioned(token);
|
surface.send_repositioned(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {
|
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
|
||||||
// FIXME popup grabs
|
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
|
||||||
|
// a grab. It will likely need refactors in Smithay to support properly since grabs just
|
||||||
|
// replace each other.
|
||||||
|
// FIXME: do this properly.
|
||||||
|
if self.niri.seat.input_method().keyboard_grabbed() {
|
||||||
|
trace!("ignoring popup grab because IME has keyboard grabbed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let popup = PopupKind::Xdg(surface);
|
||||||
|
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need to hand out the grab in a way consistent with what update_keyboard_focus()
|
||||||
|
// thinks the current focus is, otherwise it will desync and cause weird issues with
|
||||||
|
// keyboard focus being at the wrong place.
|
||||||
|
if self.niri.is_locked() {
|
||||||
|
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if self.niri.screenshot_ui.is_open() {
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
} else if let Some(output) = self.niri.layout.active_output() {
|
||||||
|
let layers = layer_map_for_output(output);
|
||||||
|
|
||||||
|
if let Some(layer_surface) =
|
||||||
|
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||||
|
{
|
||||||
|
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if layers
|
||||||
|
.layers_on(Layer::Overlay)
|
||||||
|
.any(|l| l.can_receive_keyboard_focus())
|
||||||
|
{
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mon = self.niri.layout.monitor_for_output(output).unwrap();
|
||||||
|
if !mon.render_above_top_layer()
|
||||||
|
&& layers
|
||||||
|
.layers_on(Layer::Top)
|
||||||
|
.any(|l| l.can_receive_keyboard_focus())
|
||||||
|
{
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_focus = self.niri.layout.focus();
|
||||||
|
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seat = &self.niri.seat;
|
||||||
|
let Ok(mut grab) = self
|
||||||
|
.niri
|
||||||
|
.popups
|
||||||
|
.grab_popup(root.clone(), popup, seat, serial)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let keyboard = seat.get_keyboard().unwrap();
|
||||||
|
let pointer = seat.get_pointer().unwrap();
|
||||||
|
|
||||||
|
let keyboard_grab_mismatches = keyboard.is_grabbed()
|
||||||
|
&& !(keyboard.has_grab(serial)
|
||||||
|
|| grab
|
||||||
|
.previous_serial()
|
||||||
|
.map_or(true, |s| keyboard.has_grab(s)));
|
||||||
|
let pointer_grab_mismatches = pointer.is_grabbed()
|
||||||
|
&& !(pointer.has_grab(serial)
|
||||||
|
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
|
||||||
|
if keyboard_grab_mismatches || pointer_grab_mismatches {
|
||||||
|
grab.ungrab(PopupUngrabStrategy::All);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("new grab for root {:?}", root);
|
||||||
|
keyboard.set_focus(self, grab.current_grab(), serial);
|
||||||
|
keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
|
||||||
|
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
|
||||||
|
self.niri.popup_grab = Some(PopupGrabState { root, grab });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
@@ -112,16 +214,15 @@ impl XdgShellHandler for State {
|
|||||||
.capabilities
|
.capabilities
|
||||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||||
{
|
{
|
||||||
// NOTE: This is only one part of the solution. We can set the
|
|
||||||
// location and configure size here, but the surface should be rendered fullscreen
|
|
||||||
// independently from its buffer size
|
|
||||||
if let Some((window, current_output)) = self
|
if let Some((window, current_output)) = self
|
||||||
.niri
|
.niri
|
||||||
.layout
|
.layout
|
||||||
.find_window_and_output(surface.wl_surface())
|
.find_window_and_output(surface.wl_surface())
|
||||||
{
|
{
|
||||||
|
let window = window.clone();
|
||||||
|
|
||||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||||
if requested_output != current_output {
|
if &requested_output != current_output {
|
||||||
self.niri
|
self.niri
|
||||||
.layout
|
.layout
|
||||||
.move_window_to_output(window.clone(), &requested_output);
|
.move_window_to_output(window.clone(), &requested_output);
|
||||||
@@ -143,6 +244,7 @@ impl XdgShellHandler for State {
|
|||||||
.layout
|
.layout
|
||||||
.find_window_and_output(surface.wl_surface())
|
.find_window_and_output(surface.wl_surface())
|
||||||
{
|
{
|
||||||
|
let window = window.clone();
|
||||||
self.niri.layout.set_fullscreen(&window, false);
|
self.niri.layout.set_fullscreen(&window, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +265,7 @@ impl XdgShellHandler for State {
|
|||||||
.layout
|
.layout
|
||||||
.find_window_and_output(surface.wl_surface());
|
.find_window_and_output(surface.wl_surface());
|
||||||
|
|
||||||
let Some((window, output)) = win_out else {
|
let Some((window, output)) = win_out.map(clone2) else {
|
||||||
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
|
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
|
||||||
// involving laptop going to sleep and resuming.
|
// involving laptop going to sleep and resuming.
|
||||||
error!("toplevel missing from both unmapped_windows and layout");
|
error!("toplevel missing from both unmapped_windows and layout");
|
||||||
@@ -175,11 +277,8 @@ impl XdgShellHandler for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn popup_destroyed(&mut self, surface: PopupSurface) {
|
fn popup_destroyed(&mut self, surface: PopupSurface) {
|
||||||
if let Ok(root) = find_popup_root_surface(&surface.into()) {
|
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
|
||||||
let root_window_output = self.niri.layout.find_window_and_output(&root);
|
self.niri.queue_redraw(output.clone());
|
||||||
if let Some((_window, output)) = root_window_output {
|
|
||||||
self.niri.queue_redraw(output);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,40 +287,42 @@ delegate_xdg_shell!(State);
|
|||||||
|
|
||||||
impl XdgDecorationHandler for State {
|
impl XdgDecorationHandler for State {
|
||||||
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
|
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
|
||||||
let mode = if self.niri.config.borrow().prefer_no_csd {
|
// If we want CSD, we hide this global altogether.
|
||||||
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
toplevel.with_pending_state(|state| {
|
toplevel.with_pending_state(|state| {
|
||||||
state.decoration_mode = mode;
|
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_mode(&mut self, toplevel: ToplevelSurface, mode: zxdg_toplevel_decoration_v1::Mode) {
|
fn request_mode(&mut self, toplevel: ToplevelSurface, mode: zxdg_toplevel_decoration_v1::Mode) {
|
||||||
|
// Set whatever the client wants, rather than our preferred mode. This especially matters
|
||||||
|
// for SDL2 which has a bug where forcing a different (client-side) decoration mode during
|
||||||
|
// their window creation sequence would leave the window permanently hidden.
|
||||||
|
//
|
||||||
|
// https://github.com/libsdl-org/SDL/issues/8173
|
||||||
|
//
|
||||||
|
// The bug has been fixed, but there's a ton of apps which will use the buggy version for a
|
||||||
|
// long while...
|
||||||
toplevel.with_pending_state(|state| {
|
toplevel.with_pending_state(|state| {
|
||||||
state.decoration_mode = Some(mode);
|
state.decoration_mode = Some(mode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only send configure if it's non-initial.
|
// A configure is required in response to this event. However, if an initial configure
|
||||||
|
// wasn't sent, then we will send this as part of the initial configure later.
|
||||||
if initial_configure_sent(&toplevel) {
|
if initial_configure_sent(&toplevel) {
|
||||||
toplevel.send_pending_configure();
|
toplevel.send_configure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
|
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
|
||||||
let mode = if self.niri.config.borrow().prefer_no_csd {
|
// If we want CSD, we hide this global altogether.
|
||||||
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
toplevel.with_pending_state(|state| {
|
toplevel.with_pending_state(|state| {
|
||||||
state.decoration_mode = mode;
|
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only send configure if it's non-initial.
|
// A configure is required in response to this event. However, if an initial configure
|
||||||
|
// wasn't sent, then we will send this as part of the initial configure later.
|
||||||
if initial_configure_sent(&toplevel) {
|
if initial_configure_sent(&toplevel) {
|
||||||
toplevel.send_pending_configure();
|
toplevel.send_configure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,12 +372,8 @@ impl State {
|
|||||||
.initial_configure_sent
|
.initial_configure_sent
|
||||||
});
|
});
|
||||||
if !initial_configure_sent {
|
if !initial_configure_sent {
|
||||||
if let Some(output) = popup.get_parent_surface().and_then(|parent| {
|
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
|
||||||
self.niri
|
{
|
||||||
.layout
|
|
||||||
.find_window_and_output(&parent)
|
|
||||||
.map(|(_, output)| output)
|
|
||||||
}) {
|
|
||||||
let scale = output.current_scale().integer_scale();
|
let scale = output.current_scale().integer_scale();
|
||||||
let transform = output.current_transform();
|
let transform = output.current_transform();
|
||||||
with_states(surface, |data| {
|
with_states(surface, |data| {
|
||||||
@@ -291,4 +388,132 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn output_for_popup(&self, popup: &PopupKind) -> Option<&Output> {
|
||||||
|
let root = find_popup_root_surface(popup).ok()?;
|
||||||
|
self.niri.output_for_root(&root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unconstrain_popup(&self, popup: &PopupSurface) {
|
||||||
|
let _span = tracy_client::span!("Niri::unconstrain_popup");
|
||||||
|
|
||||||
|
// Popups with a NULL parent will get repositioned in their respective protocol handlers
|
||||||
|
// (i.e. layer-shell).
|
||||||
|
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Figure out if the root is a window or a layer surface.
|
||||||
|
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
|
||||||
|
self.unconstrain_window_popup(popup, window, output);
|
||||||
|
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
|
||||||
|
let map = layer_map_for_output(o);
|
||||||
|
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||||
|
Some((layer_surface.clone(), o))
|
||||||
|
}) {
|
||||||
|
self.unconstrain_layer_shell_popup(popup, &layer_surface, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unconstrain_window_popup(&self, popup: &PopupSurface, window: &Window, output: &Output) {
|
||||||
|
let window_geo = window.geometry();
|
||||||
|
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||||
|
|
||||||
|
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||||
|
// we will compute that here.
|
||||||
|
//
|
||||||
|
// We try to keep regular window popups within the window itself horizontally (since the
|
||||||
|
// window can be scrolled to both edges of the screen), but within the whole monitor's
|
||||||
|
// height.
|
||||||
|
let mut target =
|
||||||
|
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h));
|
||||||
|
target.loc.y -= self.niri.layout.window_y(window).unwrap();
|
||||||
|
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
|
||||||
|
|
||||||
|
popup.with_pending_state(|state| {
|
||||||
|
state.geometry = unconstrain_with_padding(state.positioner, target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unconstrain_layer_shell_popup(
|
||||||
|
&self,
|
||||||
|
popup: &PopupSurface,
|
||||||
|
layer_surface: &LayerSurface,
|
||||||
|
output: &Output,
|
||||||
|
) {
|
||||||
|
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||||
|
let map = layer_map_for_output(output);
|
||||||
|
let Some(layer_geo) = map.layer_geometry(layer_surface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||||
|
// we will compute that here.
|
||||||
|
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
|
||||||
|
target.loc -= layer_geo.loc;
|
||||||
|
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
|
||||||
|
|
||||||
|
popup.with_pending_state(|state| {
|
||||||
|
state.geometry = unconstrain_with_padding(state.positioner, target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
|
||||||
|
let _span = tracy_client::span!("Niri::update_reactive_popups");
|
||||||
|
|
||||||
|
for (popup, _) in PopupManager::popups_for_surface(window.toplevel().wl_surface()) {
|
||||||
|
match popup {
|
||||||
|
PopupKind::Xdg(ref popup) => {
|
||||||
|
if popup.with_pending_state(|state| state.positioner.reactive) {
|
||||||
|
self.unconstrain_window_popup(popup, window, output);
|
||||||
|
if let Err(err) = popup.send_pending_configure() {
|
||||||
|
warn!("error re-configuring reactive popup: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PopupKind::InputMethod(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unconstrain_with_padding(
|
||||||
|
positioner: PositionerState,
|
||||||
|
target: Rectangle<i32, Logical>,
|
||||||
|
) -> Rectangle<i32, Logical> {
|
||||||
|
// Try unconstraining with a small padding first which looks nicer, then if it doesn't fit try
|
||||||
|
// unconstraining without padding.
|
||||||
|
const PADDING: i32 = 8;
|
||||||
|
|
||||||
|
let mut padded = target;
|
||||||
|
if PADDING * 2 < padded.size.w {
|
||||||
|
padded.loc.x += PADDING;
|
||||||
|
padded.size.w -= PADDING * 2;
|
||||||
|
}
|
||||||
|
if PADDING * 2 < padded.size.h {
|
||||||
|
padded.loc.y += PADDING;
|
||||||
|
padded.size.h -= PADDING * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No padding, so just unconstrain with the original target.
|
||||||
|
if padded == target {
|
||||||
|
return positioner.get_unconstrained_geometry(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not try to resize to fit the padded target rectangle.
|
||||||
|
let mut no_resize = positioner;
|
||||||
|
no_resize
|
||||||
|
.constraint_adjustment
|
||||||
|
.remove(ConstraintAdjustment::ResizeX);
|
||||||
|
no_resize
|
||||||
|
.constraint_adjustment
|
||||||
|
.remove(ConstraintAdjustment::ResizeY);
|
||||||
|
|
||||||
|
let geo = no_resize.get_unconstrained_geometry(padded);
|
||||||
|
if padded.contains_rect(geo) {
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not unconstrain into the padded target, so resort to the regular one.
|
||||||
|
positioner.get_unconstrained_geometry(target)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::iter::zip;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use niri_config::{Action, Config, Key, Modifiers};
|
||||||
|
use pangocairo::cairo::{self, ImageSurface};
|
||||||
|
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
|
||||||
|
use smithay::backend::renderer::element::memory::{
|
||||||
|
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||||
|
use smithay::backend::renderer::element::Kind;
|
||||||
|
use smithay::input::keyboard::xkb::keysym_get_name;
|
||||||
|
use smithay::output::{Output, WeakOutput};
|
||||||
|
use smithay::reexports::gbm::Format as Fourcc;
|
||||||
|
use smithay::utils::{Physical, Size, Transform};
|
||||||
|
|
||||||
|
use crate::input::CompositorMod;
|
||||||
|
use crate::render_helpers::NiriRenderer;
|
||||||
|
|
||||||
|
const PADDING: i32 = 8;
|
||||||
|
const MARGIN: i32 = PADDING * 2;
|
||||||
|
const FONT: &str = "sans 14px";
|
||||||
|
const BORDER: i32 = 4;
|
||||||
|
const LINE_INTERVAL: i32 = 2;
|
||||||
|
const TITLE: &str = "Important Hotkeys";
|
||||||
|
|
||||||
|
pub struct HotkeyOverlay {
|
||||||
|
is_open: bool,
|
||||||
|
config: Rc<RefCell<Config>>,
|
||||||
|
comp_mod: CompositorMod,
|
||||||
|
buffers: RefCell<HashMap<WeakOutput, RenderedOverlay>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RenderedOverlay {
|
||||||
|
buffer: Option<MemoryRenderBuffer>,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
scale: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type HotkeyOverlayRenderElement<R> = RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
|
||||||
|
|
||||||
|
impl HotkeyOverlay {
|
||||||
|
pub fn new(config: Rc<RefCell<Config>>, comp_mod: CompositorMod) -> Self {
|
||||||
|
Self {
|
||||||
|
is_open: false,
|
||||||
|
config,
|
||||||
|
comp_mod,
|
||||||
|
buffers: RefCell::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self) -> bool {
|
||||||
|
if !self.is_open {
|
||||||
|
self.is_open = true;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide(&mut self) -> bool {
|
||||||
|
if self.is_open {
|
||||||
|
self.is_open = false;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.is_open
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_hotkey_config_updated(&mut self) {
|
||||||
|
self.buffers.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
output: &Output,
|
||||||
|
) -> Option<HotkeyOverlayRenderElement<R>> {
|
||||||
|
if !self.is_open {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = output.current_scale().integer_scale();
|
||||||
|
let margin = MARGIN * scale;
|
||||||
|
|
||||||
|
let output_transform = output.current_transform();
|
||||||
|
let output_mode = output.current_mode().unwrap();
|
||||||
|
let output_size = output_transform.transform_size(output_mode.size);
|
||||||
|
|
||||||
|
let mut buffers = self.buffers.borrow_mut();
|
||||||
|
buffers.retain(|output, _| output.upgrade().is_some());
|
||||||
|
|
||||||
|
// FIXME: should probably use the working area rather than view size.
|
||||||
|
let weak = output.downgrade();
|
||||||
|
if let Some(rendered) = buffers.get(&weak) {
|
||||||
|
if rendered.scale != scale {
|
||||||
|
buffers.remove(&weak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rendered = buffers.entry(weak).or_insert_with(|| {
|
||||||
|
render(&self.config.borrow(), self.comp_mod, scale).unwrap_or_else(|_| {
|
||||||
|
// This can go negative but whatever, as long as there's no rerender loop.
|
||||||
|
let mut size = output_size;
|
||||||
|
size.w -= margin * 2;
|
||||||
|
size.h -= margin * 2;
|
||||||
|
RenderedOverlay {
|
||||||
|
buffer: None,
|
||||||
|
size,
|
||||||
|
scale,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let buffer = rendered.buffer.as_ref()?;
|
||||||
|
|
||||||
|
let elem = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
|
renderer,
|
||||||
|
(0., 0.),
|
||||||
|
buffer,
|
||||||
|
Some(0.9),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let x = (output_size.w / 2 - rendered.size.w / 2).max(0);
|
||||||
|
let y = (output_size.h / 2 - rendered.size.h / 2).max(0);
|
||||||
|
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
|
||||||
|
|
||||||
|
Some(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Result<RenderedOverlay> {
|
||||||
|
let _span = tracy_client::span!("hotkey_overlay::render");
|
||||||
|
|
||||||
|
// let margin = MARGIN * scale;
|
||||||
|
let padding = PADDING * scale;
|
||||||
|
let line_interval = LINE_INTERVAL * scale;
|
||||||
|
|
||||||
|
// FIXME: if it doesn't fit, try splitting in two columns or something.
|
||||||
|
// let mut target_size = output_size;
|
||||||
|
// target_size.w -= margin * 2;
|
||||||
|
// target_size.h -= margin * 2;
|
||||||
|
// anyhow::ensure!(target_size.w > 0 && target_size.h > 0);
|
||||||
|
|
||||||
|
let binds = &config.binds.0;
|
||||||
|
|
||||||
|
// Collect actions that we want to show.
|
||||||
|
let mut actions = vec![
|
||||||
|
&Action::ShowHotkeyOverlay,
|
||||||
|
&Action::Quit,
|
||||||
|
&Action::CloseWindow,
|
||||||
|
];
|
||||||
|
|
||||||
|
actions.extend(&[
|
||||||
|
&Action::FocusColumnLeft,
|
||||||
|
&Action::FocusColumnRight,
|
||||||
|
&Action::MoveColumnLeft,
|
||||||
|
&Action::MoveColumnRight,
|
||||||
|
&Action::FocusWorkspaceDown,
|
||||||
|
&Action::FocusWorkspaceUp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
|
||||||
|
if binds
|
||||||
|
.iter()
|
||||||
|
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceDown))
|
||||||
|
{
|
||||||
|
actions.push(&Action::MoveColumnToWorkspaceDown);
|
||||||
|
} else if binds
|
||||||
|
.iter()
|
||||||
|
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceDown))
|
||||||
|
{
|
||||||
|
actions.push(&Action::MoveWindowToWorkspaceDown);
|
||||||
|
} else {
|
||||||
|
actions.push(&Action::MoveColumnToWorkspaceDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same for -up.
|
||||||
|
if binds
|
||||||
|
.iter()
|
||||||
|
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceUp))
|
||||||
|
{
|
||||||
|
actions.push(&Action::MoveColumnToWorkspaceUp);
|
||||||
|
} else if binds
|
||||||
|
.iter()
|
||||||
|
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceUp))
|
||||||
|
{
|
||||||
|
actions.push(&Action::MoveWindowToWorkspaceUp);
|
||||||
|
} else {
|
||||||
|
actions.push(&Action::MoveColumnToWorkspaceUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.extend(&[
|
||||||
|
&Action::SwitchPresetColumnWidth,
|
||||||
|
&Action::MaximizeColumn,
|
||||||
|
&Action::ConsumeWindowIntoColumn,
|
||||||
|
&Action::ExpelWindowFromColumn,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Screenshot is not as important, can omit if not bound.
|
||||||
|
if binds
|
||||||
|
.iter()
|
||||||
|
.any(|bind| bind.actions.first() == Some(&Action::Screenshot))
|
||||||
|
{
|
||||||
|
actions.push(&Action::Screenshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the spawn actions.
|
||||||
|
for bind in binds
|
||||||
|
.iter()
|
||||||
|
.filter(|bind| matches!(bind.actions.first(), Some(Action::Spawn(_))))
|
||||||
|
{
|
||||||
|
actions.push(bind.actions.first().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let strings = actions
|
||||||
|
.into_iter()
|
||||||
|
.map(|action| {
|
||||||
|
let key = config
|
||||||
|
.binds
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.find(|bind| bind.actions.first() == Some(action))
|
||||||
|
.map(|bind| key_name(comp_mod, &bind.key))
|
||||||
|
.unwrap_or_else(|| String::from("(not bound)"));
|
||||||
|
|
||||||
|
(format!(" {key} "), action_name(action))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut font = FontDescription::from_string(FONT);
|
||||||
|
font.set_absolute_size((font.size() * scale).into());
|
||||||
|
|
||||||
|
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||||
|
let cr = cairo::Context::new(&surface)?;
|
||||||
|
let layout = pangocairo::create_layout(&cr);
|
||||||
|
layout.set_font_description(Some(&font));
|
||||||
|
|
||||||
|
let bold = AttrList::new();
|
||||||
|
bold.insert(AttrInt::new_weight(Weight::Bold));
|
||||||
|
layout.set_attributes(Some(&bold));
|
||||||
|
layout.set_text(TITLE);
|
||||||
|
let title_size = layout.pixel_size();
|
||||||
|
|
||||||
|
let attrs = AttrList::new();
|
||||||
|
attrs.insert(AttrString::new_family("Monospace"));
|
||||||
|
attrs.insert(AttrColor::new_background(12000, 12000, 12000));
|
||||||
|
|
||||||
|
layout.set_attributes(Some(&attrs));
|
||||||
|
let key_sizes = strings
|
||||||
|
.iter()
|
||||||
|
.map(|(key, _)| {
|
||||||
|
layout.set_text(key);
|
||||||
|
layout.pixel_size()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
layout.set_attributes(None);
|
||||||
|
let action_sizes = strings
|
||||||
|
.iter()
|
||||||
|
.map(|(_, action)| {
|
||||||
|
layout.set_markup(action);
|
||||||
|
layout.pixel_size()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let key_width = key_sizes.iter().map(|(w, _)| w).max().unwrap();
|
||||||
|
let action_width = action_sizes.iter().map(|(w, _)| w).max().unwrap();
|
||||||
|
let mut width = key_width + padding + action_width;
|
||||||
|
|
||||||
|
let mut height = zip(&key_sizes, &action_sizes)
|
||||||
|
.map(|((_, key_h), (_, act_h))| max(key_h, act_h))
|
||||||
|
.sum::<i32>()
|
||||||
|
+ (key_sizes.len() - 1) as i32 * line_interval
|
||||||
|
+ title_size.1
|
||||||
|
+ padding;
|
||||||
|
|
||||||
|
width += padding * 2;
|
||||||
|
height += padding * 2;
|
||||||
|
|
||||||
|
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
|
||||||
|
width = (width + scale - 1) / scale * scale;
|
||||||
|
height = (height + scale - 1) / scale * scale;
|
||||||
|
|
||||||
|
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
|
||||||
|
let cr = cairo::Context::new(&surface)?;
|
||||||
|
cr.set_source_rgb(0.1, 0.1, 0.1);
|
||||||
|
cr.paint()?;
|
||||||
|
|
||||||
|
cr.move_to(padding.into(), padding.into());
|
||||||
|
let layout = pangocairo::create_layout(&cr);
|
||||||
|
layout.set_font_description(Some(&font));
|
||||||
|
|
||||||
|
cr.set_source_rgb(1., 1., 1.);
|
||||||
|
|
||||||
|
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
|
||||||
|
layout.set_attributes(Some(&bold));
|
||||||
|
layout.set_text(TITLE);
|
||||||
|
pangocairo::show_layout(&cr, &layout);
|
||||||
|
|
||||||
|
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
|
||||||
|
|
||||||
|
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
|
||||||
|
layout.set_attributes(Some(&attrs));
|
||||||
|
layout.set_text(key);
|
||||||
|
pangocairo::show_layout(&cr, &layout);
|
||||||
|
|
||||||
|
cr.rel_move_to((key_width + padding).into(), 0.);
|
||||||
|
|
||||||
|
layout.set_attributes(None);
|
||||||
|
layout.set_markup(action);
|
||||||
|
pangocairo::show_layout(&cr, &layout);
|
||||||
|
|
||||||
|
cr.rel_move_to(
|
||||||
|
(-(key_width + padding)).into(),
|
||||||
|
(max(key_h, act_h) + line_interval).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.move_to(0., 0.);
|
||||||
|
cr.line_to(width.into(), 0.);
|
||||||
|
cr.line_to(width.into(), height.into());
|
||||||
|
cr.line_to(0., height.into());
|
||||||
|
cr.line_to(0., 0.);
|
||||||
|
cr.set_source_rgb(0.5, 0.8, 1.0);
|
||||||
|
cr.set_line_width((BORDER * scale).into());
|
||||||
|
cr.stroke()?;
|
||||||
|
drop(cr);
|
||||||
|
|
||||||
|
let data = surface.take_data().unwrap();
|
||||||
|
let buffer = MemoryRenderBuffer::from_slice(
|
||||||
|
&data,
|
||||||
|
Fourcc::Argb8888,
|
||||||
|
(width, height),
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(RenderedOverlay {
|
||||||
|
buffer: Some(buffer),
|
||||||
|
size: Size::from((width, height)),
|
||||||
|
scale,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_name(action: &Action) -> String {
|
||||||
|
match action {
|
||||||
|
Action::Quit => String::from("Exit niri"),
|
||||||
|
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
|
||||||
|
Action::CloseWindow => String::from("Close Focused Window"),
|
||||||
|
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
|
||||||
|
Action::FocusColumnRight => String::from("Focus Column to the Right"),
|
||||||
|
Action::MoveColumnLeft => String::from("Move Column Left"),
|
||||||
|
Action::MoveColumnRight => String::from("Move Column Right"),
|
||||||
|
Action::FocusWorkspaceDown => String::from("Switch Workspace Down"),
|
||||||
|
Action::FocusWorkspaceUp => String::from("Switch Workspace Up"),
|
||||||
|
Action::MoveColumnToWorkspaceDown => String::from("Move Column to Workspace Down"),
|
||||||
|
Action::MoveColumnToWorkspaceUp => String::from("Move Column to Workspace Up"),
|
||||||
|
Action::MoveWindowToWorkspaceDown => String::from("Move Window to Workspace Down"),
|
||||||
|
Action::MoveWindowToWorkspaceUp => String::from("Move Window to Workspace Up"),
|
||||||
|
Action::SwitchPresetColumnWidth => String::from("Switch Preset Column Widths"),
|
||||||
|
Action::MaximizeColumn => String::from("Maximize Column"),
|
||||||
|
Action::ConsumeWindowIntoColumn => String::from("Consume Window Into Column"),
|
||||||
|
Action::ExpelWindowFromColumn => String::from("Expel Window From Column"),
|
||||||
|
Action::Screenshot => String::from("Take a Screenshot"),
|
||||||
|
Action::Spawn(args) => format!(
|
||||||
|
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
|
||||||
|
args.first().unwrap_or(&String::new())
|
||||||
|
),
|
||||||
|
_ => String::from("FIXME: Unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
|
||||||
|
let mut name = String::new();
|
||||||
|
|
||||||
|
let has_comp_mod = key.modifiers.contains(Modifiers::COMPOSITOR);
|
||||||
|
|
||||||
|
if key.modifiers.contains(Modifiers::SUPER)
|
||||||
|
|| (has_comp_mod && comp_mod == CompositorMod::Super)
|
||||||
|
{
|
||||||
|
name.push_str("Super + ");
|
||||||
|
}
|
||||||
|
if key.modifiers.contains(Modifiers::ALT) || (has_comp_mod && comp_mod == CompositorMod::Alt) {
|
||||||
|
name.push_str("Alt + ");
|
||||||
|
}
|
||||||
|
if key.modifiers.contains(Modifiers::SHIFT) {
|
||||||
|
name.push_str("Shift + ");
|
||||||
|
}
|
||||||
|
if key.modifiers.contains(Modifiers::CTRL) {
|
||||||
|
name.push_str("Ctrl + ");
|
||||||
|
}
|
||||||
|
name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym)));
|
||||||
|
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prettify_keysym_name(name: &str) -> String {
|
||||||
|
let name = match name {
|
||||||
|
"slash" => "/",
|
||||||
|
"comma" => ",",
|
||||||
|
"period" => ".",
|
||||||
|
"minus" => "-",
|
||||||
|
"equal" => "=",
|
||||||
|
"grave" => "`",
|
||||||
|
"Next" => "Page Down",
|
||||||
|
"Prior" => "Page Up",
|
||||||
|
"Print" => "PrtSc",
|
||||||
|
"Return" => "Enter",
|
||||||
|
_ => name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if name.len() == 1 && name.is_ascii() {
|
||||||
|
name.to_ascii_uppercase()
|
||||||
|
} else {
|
||||||
|
name.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
+1280
-831
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::Shutdown;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context};
|
||||||
|
use niri_ipc::{Mode, Output, Request, Response};
|
||||||
|
|
||||||
|
use crate::Msg;
|
||||||
|
|
||||||
|
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||||
|
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"{} is not set, are you running this within niri?",
|
||||||
|
niri_ipc::SOCKET_PATH_ENV
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut stream =
|
||||||
|
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
|
||||||
|
|
||||||
|
let request = match msg {
|
||||||
|
Msg::Outputs => Request::Outputs,
|
||||||
|
};
|
||||||
|
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||||
|
stream
|
||||||
|
.write_all(&buf)
|
||||||
|
.context("error writing IPC request")?;
|
||||||
|
stream
|
||||||
|
.shutdown(Shutdown::Write)
|
||||||
|
.context("error closing IPC stream for writing")?;
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
stream
|
||||||
|
.read_to_end(&mut buf)
|
||||||
|
.context("error reading IPC response")?;
|
||||||
|
|
||||||
|
let response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
|
||||||
|
match msg {
|
||||||
|
Msg::Outputs => {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
|
let Response::Outputs(outputs) = response
|
||||||
|
else {
|
||||||
|
bail!("unexpected response: expected Outputs, got {response:?}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let output =
|
||||||
|
serde_json::to_string(&outputs).context("error formatting response")?;
|
||||||
|
println!("{output}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
|
||||||
|
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
for (connector, output) in outputs.into_iter() {
|
||||||
|
let Output {
|
||||||
|
name,
|
||||||
|
make,
|
||||||
|
model,
|
||||||
|
physical_size,
|
||||||
|
modes,
|
||||||
|
current_mode,
|
||||||
|
} = output;
|
||||||
|
|
||||||
|
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
|
||||||
|
|
||||||
|
if let Some(current) = current_mode {
|
||||||
|
let mode = *modes
|
||||||
|
.get(current)
|
||||||
|
.context("invalid response: current mode does not exist")?;
|
||||||
|
let Mode {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
refresh_rate,
|
||||||
|
} = mode;
|
||||||
|
let refresh = refresh_rate as f64 / 1000.;
|
||||||
|
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz");
|
||||||
|
} else {
|
||||||
|
println!(" Disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((width, height)) = physical_size {
|
||||||
|
println!(" Physical size: {width}x{height} mm");
|
||||||
|
} else {
|
||||||
|
println!(" Physical size: unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" Available modes:");
|
||||||
|
for mode in modes {
|
||||||
|
let Mode {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
refresh_rate,
|
||||||
|
} = mode;
|
||||||
|
let refresh = refresh_rate as f64 / 1000.;
|
||||||
|
println!(" {width}x{height}@{refresh:.3}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod server;
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::{env, io, process};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use calloop::io::Async;
|
||||||
|
use directories::BaseDirs;
|
||||||
|
use futures_util::io::{AsyncReadExt, BufReader};
|
||||||
|
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
|
||||||
|
use niri_ipc::{Request, Response};
|
||||||
|
use smithay::reexports::calloop::generic::Generic;
|
||||||
|
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||||
|
use smithay::reexports::rustix::fs::unlink;
|
||||||
|
|
||||||
|
use crate::niri::State;
|
||||||
|
|
||||||
|
pub struct IpcServer {
|
||||||
|
pub socket_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ClientCtx {
|
||||||
|
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpcServer {
|
||||||
|
pub fn start(
|
||||||
|
event_loop: &LoopHandle<'static, State>,
|
||||||
|
wayland_socket_name: &str,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let _span = tracy_client::span!("Ipc::start");
|
||||||
|
|
||||||
|
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
||||||
|
let mut socket_path = socket_dir();
|
||||||
|
socket_path.push(socket_name);
|
||||||
|
|
||||||
|
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
||||||
|
listener
|
||||||
|
.set_nonblocking(true)
|
||||||
|
.context("error setting socket to non-blocking")?;
|
||||||
|
|
||||||
|
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
||||||
|
event_loop
|
||||||
|
.insert_source(source, |_, socket, state| {
|
||||||
|
match socket.accept() {
|
||||||
|
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PostAction::Continue)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Self { socket_path })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for IpcServer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = unlink(&self.socket_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_dir() -> PathBuf {
|
||||||
|
BaseDirs::new()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|x| x.runtime_dir())
|
||||||
|
.map(|x| x.to_owned())
|
||||||
|
.unwrap_or_else(env::temp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||||
|
let _span = tracy_client::span!("on_new_ipc_client");
|
||||||
|
trace!("new IPC client connected");
|
||||||
|
|
||||||
|
let stream = match state.niri.event_loop.adapt_io(stream) {
|
||||||
|
Ok(stream) => stream,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error making IPC stream async: {err:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ctx = ClientCtx {
|
||||||
|
ipc_outputs: state.backend.ipc_outputs(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let future = async move {
|
||||||
|
if let Err(err) = handle_client(ctx, stream).await {
|
||||||
|
warn!("error handling IPC client: {err:?}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = state.niri.scheduler.schedule(future) {
|
||||||
|
warn!("error scheduling IPC stream future: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> {
|
||||||
|
let (read, mut write) = stream.split();
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
// Read a single line to allow extensibility in the future to keep reading.
|
||||||
|
BufReader::new(read)
|
||||||
|
.read_line(&mut buf)
|
||||||
|
.await
|
||||||
|
.context("error reading request")?;
|
||||||
|
|
||||||
|
let request: Request = serde_json::from_str(&buf).context("error parsing request")?;
|
||||||
|
|
||||||
|
let response = match request {
|
||||||
|
Request::Outputs => {
|
||||||
|
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
|
||||||
|
Response::Outputs(ipc_outputs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let buf = serde_json::to_vec(&response).context("error formatting response")?;
|
||||||
|
write
|
||||||
|
.write_all(&buf)
|
||||||
|
.await
|
||||||
|
.context("error writing response")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
-3870
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
|||||||
|
use std::iter::zip;
|
||||||
|
|
||||||
|
use arrayvec::ArrayVec;
|
||||||
|
use niri_config::{self, Color};
|
||||||
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
|
use smithay::backend::renderer::element::Kind;
|
||||||
|
use smithay::utils::{Logical, Point, Scale, Size};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FocusRing {
|
||||||
|
buffers: [SolidColorBuffer; 4],
|
||||||
|
locations: [Point<i32, Logical>; 4],
|
||||||
|
is_off: bool,
|
||||||
|
is_border: bool,
|
||||||
|
width: i32,
|
||||||
|
active_color: Color,
|
||||||
|
inactive_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type FocusRingRenderElement = SolidColorRenderElement;
|
||||||
|
|
||||||
|
impl FocusRing {
|
||||||
|
pub fn new(config: niri_config::FocusRing) -> Self {
|
||||||
|
Self {
|
||||||
|
buffers: Default::default(),
|
||||||
|
locations: Default::default(),
|
||||||
|
is_off: config.off,
|
||||||
|
is_border: false,
|
||||||
|
width: config.width.into(),
|
||||||
|
active_color: config.active_color,
|
||||||
|
inactive_color: config.inactive_color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, config: niri_config::FocusRing) {
|
||||||
|
self.is_off = config.off;
|
||||||
|
self.width = config.width.into();
|
||||||
|
self.active_color = config.active_color;
|
||||||
|
self.inactive_color = config.inactive_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
win_pos: Point<i32, Logical>,
|
||||||
|
win_size: Size<i32, Logical>,
|
||||||
|
is_border: bool,
|
||||||
|
) {
|
||||||
|
if is_border {
|
||||||
|
self.buffers[0].resize((win_size.w + self.width * 2, self.width));
|
||||||
|
self.buffers[1].resize((win_size.w + self.width * 2, self.width));
|
||||||
|
self.buffers[2].resize((self.width, win_size.h));
|
||||||
|
self.buffers[3].resize((self.width, win_size.h));
|
||||||
|
|
||||||
|
self.locations[0] = win_pos + Point::from((-self.width, -self.width));
|
||||||
|
self.locations[1] = win_pos + Point::from((-self.width, win_size.h));
|
||||||
|
self.locations[2] = win_pos + Point::from((-self.width, 0));
|
||||||
|
self.locations[3] = win_pos + Point::from((win_size.w, 0));
|
||||||
|
} else {
|
||||||
|
let size = win_size + Size::from((self.width * 2, self.width * 2));
|
||||||
|
self.buffers[0].resize(size);
|
||||||
|
self.locations[0] = win_pos - Point::from((self.width, self.width));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_border = is_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active(&mut self, is_active: bool) {
|
||||||
|
let color = if is_active {
|
||||||
|
self.active_color.into()
|
||||||
|
} else {
|
||||||
|
self.inactive_color.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
for buf in &mut self.buffers {
|
||||||
|
buf.set_color(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||||
|
let mut rv = ArrayVec::<_, 4>::new();
|
||||||
|
|
||||||
|
if self.is_off {
|
||||||
|
return rv.into_iter();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut push = |buffer, location: Point<i32, Logical>| {
|
||||||
|
let elem = SolidColorRenderElement::from_buffer(
|
||||||
|
buffer,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
1.,
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
rv.push(elem);
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.is_border {
|
||||||
|
for (buf, loc) in zip(&self.buffers, self.locations) {
|
||||||
|
push(buf, loc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
push(&self.buffers[0], self.locations[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> i32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_off(&self) -> bool {
|
||||||
|
self.is_off
|
||||||
|
}
|
||||||
|
}
|
||||||
+2472
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,659 @@
|
|||||||
|
use std::cmp::min;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri_config::SizeChange;
|
||||||
|
use smithay::backend::renderer::element::utils::{
|
||||||
|
CropRenderElement, Relocate, RelocateRenderElement,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||||
|
use smithay::desktop::Window;
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
||||||
|
|
||||||
|
use super::workspace::{
|
||||||
|
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
|
||||||
|
};
|
||||||
|
use super::{LayoutElement, Options};
|
||||||
|
use crate::animation::Animation;
|
||||||
|
use crate::utils::output_size;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Monitor<W: LayoutElement> {
|
||||||
|
/// Output for this monitor.
|
||||||
|
pub output: Output,
|
||||||
|
// Must always contain at least one.
|
||||||
|
pub workspaces: Vec<Workspace<W>>,
|
||||||
|
/// Index of the currently active workspace.
|
||||||
|
pub active_workspace_idx: usize,
|
||||||
|
/// In-progress switch between workspaces.
|
||||||
|
pub workspace_switch: Option<WorkspaceSwitch>,
|
||||||
|
/// Configurable properties of the layout.
|
||||||
|
pub options: Rc<Options>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum WorkspaceSwitch {
|
||||||
|
Animation(Animation),
|
||||||
|
Gesture(WorkspaceSwitchGesture),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WorkspaceSwitchGesture {
|
||||||
|
/// Index of the workspace where the gesture was started.
|
||||||
|
pub center_idx: usize,
|
||||||
|
/// Current, fractional workspace index.
|
||||||
|
pub current_idx: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MonitorRenderElement<R> =
|
||||||
|
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
|
||||||
|
|
||||||
|
impl WorkspaceSwitch {
|
||||||
|
pub fn current_idx(&self) -> f64 {
|
||||||
|
match self {
|
||||||
|
WorkspaceSwitch::Animation(anim) => anim.value(),
|
||||||
|
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the workspace switch is [`Animation`].
|
||||||
|
///
|
||||||
|
/// [`Animation`]: WorkspaceSwitch::Animation
|
||||||
|
#[must_use]
|
||||||
|
fn is_animation(&self) -> bool {
|
||||||
|
matches!(self, Self::Animation(..))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: LayoutElement> Monitor<W> {
|
||||||
|
pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
|
||||||
|
Self {
|
||||||
|
output,
|
||||||
|
workspaces,
|
||||||
|
active_workspace_idx: 0,
|
||||||
|
workspace_switch: None,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
|
||||||
|
&mut self.workspaces[self.active_workspace_idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_workspace(&mut self, idx: usize) {
|
||||||
|
if self.active_workspace_idx == idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_idx = self
|
||||||
|
.workspace_switch
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.current_idx())
|
||||||
|
.unwrap_or(self.active_workspace_idx as f64);
|
||||||
|
|
||||||
|
self.active_workspace_idx = idx;
|
||||||
|
|
||||||
|
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||||
|
current_idx,
|
||||||
|
idx as f64,
|
||||||
|
Duration::from_millis(250),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_window(
|
||||||
|
&mut self,
|
||||||
|
workspace_idx: usize,
|
||||||
|
window: W,
|
||||||
|
activate: bool,
|
||||||
|
width: ColumnWidth,
|
||||||
|
is_full_width: bool,
|
||||||
|
) {
|
||||||
|
let workspace = &mut self.workspaces[workspace_idx];
|
||||||
|
|
||||||
|
workspace.add_window(window, activate, width, is_full_width);
|
||||||
|
|
||||||
|
// After adding a new window, workspace becomes this output's own.
|
||||||
|
workspace.original_output = OutputId::new(&self.output);
|
||||||
|
|
||||||
|
if workspace_idx == self.workspaces.len() - 1 {
|
||||||
|
// Insert a new empty workspace.
|
||||||
|
let ws = Workspace::new(self.output.clone(), self.options.clone());
|
||||||
|
self.workspaces.push(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
if activate {
|
||||||
|
self.activate_workspace(workspace_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
|
||||||
|
let workspace = &mut self.workspaces[workspace_idx];
|
||||||
|
|
||||||
|
workspace.add_column(column, activate);
|
||||||
|
|
||||||
|
// After adding a new window, workspace becomes this output's own.
|
||||||
|
workspace.original_output = OutputId::new(&self.output);
|
||||||
|
|
||||||
|
if workspace_idx == self.workspaces.len() - 1 {
|
||||||
|
// Insert a new empty workspace.
|
||||||
|
let ws = Workspace::new(self.output.clone(), self.options.clone());
|
||||||
|
self.workspaces.push(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
if activate {
|
||||||
|
self.activate_workspace(workspace_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clean_up_workspaces(&mut self) {
|
||||||
|
assert!(self.workspace_switch.is_none());
|
||||||
|
|
||||||
|
for idx in (0..self.workspaces.len() - 1).rev() {
|
||||||
|
if self.active_workspace_idx == idx {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.workspaces[idx].has_windows() {
|
||||||
|
self.workspaces.remove(idx);
|
||||||
|
if self.active_workspace_idx > idx {
|
||||||
|
self.active_workspace_idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_left(&mut self) {
|
||||||
|
self.active_workspace().move_left();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_right(&mut self) {
|
||||||
|
self.active_workspace().move_right();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_first(&mut self) {
|
||||||
|
self.active_workspace().move_column_to_first();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_last(&mut self) {
|
||||||
|
self.active_workspace().move_column_to_last();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down(&mut self) {
|
||||||
|
self.active_workspace().move_down();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up(&mut self) {
|
||||||
|
self.active_workspace().move_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down_or_to_workspace_down(&mut self) {
|
||||||
|
let workspace = self.active_workspace();
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let column = &mut workspace.columns[workspace.active_column_idx];
|
||||||
|
let curr_idx = column.active_tile_idx;
|
||||||
|
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
|
||||||
|
if curr_idx == new_idx {
|
||||||
|
self.move_to_workspace_down();
|
||||||
|
} else {
|
||||||
|
workspace.move_down();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up_or_to_workspace_up(&mut self) {
|
||||||
|
let workspace = self.active_workspace();
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
|
||||||
|
let new_idx = curr_idx.saturating_sub(1);
|
||||||
|
if curr_idx == new_idx {
|
||||||
|
self.move_to_workspace_up();
|
||||||
|
} else {
|
||||||
|
workspace.move_up();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_left(&mut self) {
|
||||||
|
self.active_workspace().focus_left();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_right(&mut self) {
|
||||||
|
self.active_workspace().focus_right();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_column_first(&mut self) {
|
||||||
|
self.active_workspace().focus_column_first();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_column_last(&mut self) {
|
||||||
|
self.active_workspace().focus_column_last();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_down(&mut self) {
|
||||||
|
self.active_workspace().focus_down();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_up(&mut self) {
|
||||||
|
self.active_workspace().focus_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_window_or_workspace_down(&mut self) {
|
||||||
|
let workspace = self.active_workspace();
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
self.switch_workspace_down();
|
||||||
|
} else {
|
||||||
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
|
let curr_idx = column.active_tile_idx;
|
||||||
|
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
|
||||||
|
if curr_idx == new_idx {
|
||||||
|
self.switch_workspace_down();
|
||||||
|
} else {
|
||||||
|
workspace.focus_down();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_window_or_workspace_up(&mut self) {
|
||||||
|
let workspace = self.active_workspace();
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
self.switch_workspace_up();
|
||||||
|
} else {
|
||||||
|
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
|
||||||
|
let new_idx = curr_idx.saturating_sub(1);
|
||||||
|
if curr_idx == new_idx {
|
||||||
|
self.switch_workspace_up();
|
||||||
|
} else {
|
||||||
|
workspace.focus_up();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_to_workspace_up(&mut self) {
|
||||||
|
let source_workspace_idx = self.active_workspace_idx;
|
||||||
|
|
||||||
|
let new_idx = source_workspace_idx.saturating_sub(1);
|
||||||
|
if new_idx == source_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
|
let width = column.width;
|
||||||
|
let is_full_width = column.is_full_width;
|
||||||
|
let window =
|
||||||
|
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
|
||||||
|
|
||||||
|
self.add_window(new_idx, window, true, width, is_full_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_to_workspace_down(&mut self) {
|
||||||
|
let source_workspace_idx = self.active_workspace_idx;
|
||||||
|
|
||||||
|
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
|
||||||
|
if new_idx == source_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
|
let width = column.width;
|
||||||
|
let is_full_width = column.is_full_width;
|
||||||
|
let window =
|
||||||
|
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
|
||||||
|
|
||||||
|
self.add_window(new_idx, window, true, width, is_full_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_to_workspace(&mut self, idx: usize) {
|
||||||
|
let source_workspace_idx = self.active_workspace_idx;
|
||||||
|
|
||||||
|
let new_idx = min(idx, self.workspaces.len() - 1);
|
||||||
|
if new_idx == source_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
|
let width = column.width;
|
||||||
|
let is_full_width = column.is_full_width;
|
||||||
|
let window =
|
||||||
|
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
|
||||||
|
|
||||||
|
self.add_window(new_idx, window, true, width, is_full_width);
|
||||||
|
|
||||||
|
// Don't animate this action.
|
||||||
|
self.workspace_switch = None;
|
||||||
|
|
||||||
|
self.clean_up_workspaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_workspace_up(&mut self) {
|
||||||
|
let source_workspace_idx = self.active_workspace_idx;
|
||||||
|
|
||||||
|
let new_idx = source_workspace_idx.saturating_sub(1);
|
||||||
|
if new_idx == source_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||||
|
self.add_column(new_idx, column, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_workspace_down(&mut self) {
|
||||||
|
let source_workspace_idx = self.active_workspace_idx;
|
||||||
|
|
||||||
|
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
|
||||||
|
if new_idx == source_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||||
|
self.add_column(new_idx, column, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_workspace(&mut self, idx: usize) {
|
||||||
|
let source_workspace_idx = self.active_workspace_idx;
|
||||||
|
|
||||||
|
let new_idx = min(idx, self.workspaces.len() - 1);
|
||||||
|
if new_idx == source_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||||
|
if workspace.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||||
|
self.add_column(new_idx, column, true);
|
||||||
|
|
||||||
|
// Don't animate this action.
|
||||||
|
self.workspace_switch = None;
|
||||||
|
|
||||||
|
self.clean_up_workspaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_workspace_up(&mut self) {
|
||||||
|
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_workspace_down(&mut self) {
|
||||||
|
self.activate_workspace(min(
|
||||||
|
self.active_workspace_idx + 1,
|
||||||
|
self.workspaces.len() - 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_workspace(&mut self, idx: usize) {
|
||||||
|
self.activate_workspace(min(idx, self.workspaces.len() - 1));
|
||||||
|
// Don't animate this action.
|
||||||
|
self.workspace_switch = None;
|
||||||
|
|
||||||
|
self.clean_up_workspaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_into_column(&mut self) {
|
||||||
|
self.active_workspace().consume_into_column();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expel_from_column(&mut self) {
|
||||||
|
self.active_workspace().expel_from_column();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center_column(&mut self) {
|
||||||
|
self.active_workspace().center_column();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus(&self) -> Option<&W> {
|
||||||
|
let workspace = &self.workspaces[self.active_workspace_idx];
|
||||||
|
if !workspace.has_windows() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
|
Some(column.tiles[column.active_tile_idx].window())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
|
||||||
|
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
|
||||||
|
anim.set_current_time(current_time);
|
||||||
|
if anim.is_done() {
|
||||||
|
self.workspace_switch = None;
|
||||||
|
self.clean_up_workspaces();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ws in &mut self.workspaces {
|
||||||
|
ws.advance_animations(current_time, is_active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_animations_ongoing(&self) -> bool {
|
||||||
|
self.workspace_switch
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|s| s.is_animation())
|
||||||
|
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_transitions_ongoing(&self) -> bool {
|
||||||
|
self.workspace_switch.is_some()
|
||||||
|
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||||
|
for ws in &mut self.workspaces {
|
||||||
|
ws.update_config(options.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.options.struts != options.struts {
|
||||||
|
let view_size = output_size(&self.output);
|
||||||
|
let working_area = compute_working_area(&self.output, options.struts);
|
||||||
|
|
||||||
|
for ws in &mut self.workspaces {
|
||||||
|
ws.set_view_size(view_size, working_area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_width(&mut self) {
|
||||||
|
self.active_workspace().toggle_width();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_full_width(&mut self) {
|
||||||
|
self.active_workspace().toggle_full_width();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_column_width(&mut self, change: SizeChange) {
|
||||||
|
self.active_workspace().set_column_width(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_window_height(&mut self, change: SizeChange) {
|
||||||
|
self.active_workspace().set_window_height(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_workspace_down(&mut self) {
|
||||||
|
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
|
||||||
|
if new_idx == self.active_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.workspaces.swap(self.active_workspace_idx, new_idx);
|
||||||
|
|
||||||
|
if new_idx == self.workspaces.len() - 1 {
|
||||||
|
// Insert a new empty workspace.
|
||||||
|
let ws = Workspace::new(self.output.clone(), self.options.clone());
|
||||||
|
self.workspaces.push(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activate_workspace(new_idx);
|
||||||
|
self.workspace_switch = None;
|
||||||
|
|
||||||
|
self.clean_up_workspaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_workspace_up(&mut self) {
|
||||||
|
let new_idx = self.active_workspace_idx.saturating_sub(1);
|
||||||
|
if new_idx == self.active_workspace_idx {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.workspaces.swap(self.active_workspace_idx, new_idx);
|
||||||
|
|
||||||
|
if self.active_workspace_idx == self.workspaces.len() - 1 {
|
||||||
|
// Insert a new empty workspace.
|
||||||
|
let ws = Workspace::new(self.output.clone(), self.options.clone());
|
||||||
|
self.workspaces.push(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activate_workspace(new_idx);
|
||||||
|
self.workspace_switch = None;
|
||||||
|
|
||||||
|
self.clean_up_workspaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_under(
|
||||||
|
&self,
|
||||||
|
pos_within_output: Point<f64, Logical>,
|
||||||
|
) -> Option<(&W, Option<Point<i32, Logical>>)> {
|
||||||
|
match &self.workspace_switch {
|
||||||
|
Some(switch) => {
|
||||||
|
let size = output_size(&self.output);
|
||||||
|
|
||||||
|
let render_idx = switch.current_idx();
|
||||||
|
let before_idx = render_idx.floor() as usize;
|
||||||
|
let after_idx = render_idx.ceil() as usize;
|
||||||
|
|
||||||
|
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
|
||||||
|
|
||||||
|
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
|
||||||
|
(before_idx, Point::from((0, offset)))
|
||||||
|
} else {
|
||||||
|
(after_idx, Point::from((0, -size.h + offset)))
|
||||||
|
};
|
||||||
|
|
||||||
|
let ws = &self.workspaces[idx];
|
||||||
|
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset.to_f64())?;
|
||||||
|
Some((win, win_pos.map(|p| p - ws_offset)))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let ws = &self.workspaces[self.active_workspace_idx];
|
||||||
|
ws.window_under(pos_within_output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_above_top_layer(&self) -> bool {
|
||||||
|
// Render above the top layer only if the view is stationary.
|
||||||
|
if self.workspace_switch.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws = &self.workspaces[self.active_workspace_idx];
|
||||||
|
ws.render_above_top_layer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Monitor<Window> {
|
||||||
|
pub fn render_elements<R: Renderer + ImportAll>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
) -> Vec<MonitorRenderElement<R>>
|
||||||
|
where
|
||||||
|
<R as Renderer>::TextureId: 'static,
|
||||||
|
{
|
||||||
|
let _span = tracy_client::span!("Monitor::render_elements");
|
||||||
|
|
||||||
|
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
|
||||||
|
let output_transform = self.output.current_transform();
|
||||||
|
let output_mode = self.output.current_mode().unwrap();
|
||||||
|
let size = output_transform.transform_size(output_mode.size);
|
||||||
|
|
||||||
|
match &self.workspace_switch {
|
||||||
|
Some(switch) => {
|
||||||
|
let render_idx = switch.current_idx();
|
||||||
|
let before_idx = render_idx.floor() as usize;
|
||||||
|
let after_idx = render_idx.ceil() as usize;
|
||||||
|
|
||||||
|
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
|
||||||
|
|
||||||
|
let before = self.workspaces[before_idx].render_elements(renderer);
|
||||||
|
let after = self.workspaces[after_idx].render_elements(renderer);
|
||||||
|
|
||||||
|
let before = before.into_iter().filter_map(|elem| {
|
||||||
|
Some(RelocateRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
Rectangle::from_extemities((0, offset), (size.w, size.h)),
|
||||||
|
)?,
|
||||||
|
(0, -offset),
|
||||||
|
Relocate::Relative,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
let after = after.into_iter().filter_map(|elem| {
|
||||||
|
Some(RelocateRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
Rectangle::from_extemities((0, 0), (size.w, offset)),
|
||||||
|
)?,
|
||||||
|
(0, -offset + size.h),
|
||||||
|
Relocate::Relative,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
before.chain(after).collect()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
|
||||||
|
elements
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|elem| {
|
||||||
|
Some(RelocateRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
// HACK: set infinite crop bounds due to a damage tracking bug
|
||||||
|
// which causes glitched rendering for maximized GTK windows.
|
||||||
|
// FIXME: use proper bounds after fixing the Crop element.
|
||||||
|
Rectangle::from_loc_and_size(
|
||||||
|
(-i32::MAX / 2, -i32::MAX / 2),
|
||||||
|
(i32::MAX, i32::MAX),
|
||||||
|
),
|
||||||
|
// Rectangle::from_loc_and_size((0, 0), size),
|
||||||
|
)?,
|
||||||
|
(0, 0),
|
||||||
|
Relocate::Relative,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
use std::cmp::max;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
|
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||||
|
use smithay::backend::renderer::element::Kind;
|
||||||
|
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||||
|
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||||
|
|
||||||
|
use super::focus_ring::FocusRing;
|
||||||
|
use super::workspace::WorkspaceRenderElement;
|
||||||
|
use super::{LayoutElement, Options};
|
||||||
|
|
||||||
|
/// Toplevel window with decorations.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Tile<W: LayoutElement> {
|
||||||
|
/// The toplevel window itself.
|
||||||
|
window: W,
|
||||||
|
|
||||||
|
/// The border around the window.
|
||||||
|
border: FocusRing,
|
||||||
|
|
||||||
|
/// Whether this tile is fullscreen.
|
||||||
|
///
|
||||||
|
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
||||||
|
/// to avoid black backdrop flicker before the window has had a chance to resize.
|
||||||
|
is_fullscreen: bool,
|
||||||
|
|
||||||
|
/// The black backdrop for fullscreen windows.
|
||||||
|
fullscreen_backdrop: SolidColorBuffer,
|
||||||
|
|
||||||
|
/// The size we were requested to fullscreen into.
|
||||||
|
fullscreen_size: Size<i32, Logical>,
|
||||||
|
|
||||||
|
/// Configurable properties of the layout.
|
||||||
|
options: Rc<Options>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: LayoutElement> Tile<W> {
|
||||||
|
pub fn new(window: W, options: Rc<Options>) -> Self {
|
||||||
|
Self {
|
||||||
|
window,
|
||||||
|
border: FocusRing::new(options.border),
|
||||||
|
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
||||||
|
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
|
||||||
|
fullscreen_size: Default::default(),
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||||
|
self.border.update_config(options.border);
|
||||||
|
self.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_window(&mut self) {
|
||||||
|
// FIXME: remove when we can get a fullscreen size right away.
|
||||||
|
if self.fullscreen_size != Size::from((0, 0)) {
|
||||||
|
self.is_fullscreen = self.window.is_fullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) {
|
||||||
|
let width = self.border.width();
|
||||||
|
self.border.update(
|
||||||
|
(width, width).into(),
|
||||||
|
self.window.size(),
|
||||||
|
self.window.has_ssd(),
|
||||||
|
);
|
||||||
|
self.border.set_active(is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window(&self) -> &W {
|
||||||
|
&self.window
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_window(self) -> W {
|
||||||
|
self.window
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
|
||||||
|
fn effective_border_width(&self) -> Option<i32> {
|
||||||
|
if self.is_fullscreen {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.border.is_off() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(self.border.width())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the location of the window's visual geometry within this Tile.
|
||||||
|
pub fn window_loc(&self) -> Point<i32, Logical> {
|
||||||
|
let mut loc = Point::from((0, 0));
|
||||||
|
|
||||||
|
// In fullscreen, center the window in the given size.
|
||||||
|
if self.is_fullscreen {
|
||||||
|
let window_size = self.window.size();
|
||||||
|
let target_size = self.fullscreen_size;
|
||||||
|
|
||||||
|
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
|
||||||
|
// one, leave it at the top-left as usual.
|
||||||
|
if window_size.w < target_size.w {
|
||||||
|
loc.x += (target_size.w - window_size.w) / 2;
|
||||||
|
}
|
||||||
|
if window_size.h < target_size.h {
|
||||||
|
loc.y += (target_size.h - window_size.h) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = self.effective_border_width() {
|
||||||
|
loc += (width, width).into();
|
||||||
|
}
|
||||||
|
|
||||||
|
loc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tile_size(&self) -> Size<i32, Logical> {
|
||||||
|
let mut size = self.window.size();
|
||||||
|
|
||||||
|
if self.is_fullscreen {
|
||||||
|
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||||
|
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||||
|
size.w = max(size.w, self.fullscreen_size.w);
|
||||||
|
size.h = max(size.h, self.fullscreen_size.h);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = self.effective_border_width() {
|
||||||
|
size.w = size.w.saturating_add(width * 2);
|
||||||
|
size.h = size.h.saturating_add(width * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_size(&self) -> Size<i32, Logical> {
|
||||||
|
self.window.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buf_loc(&self) -> Point<i32, Logical> {
|
||||||
|
let mut loc = Point::from((0, 0));
|
||||||
|
loc += self.window_loc();
|
||||||
|
loc += self.window.buf_loc();
|
||||||
|
loc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
|
||||||
|
point -= self.window_loc().to_f64();
|
||||||
|
self.window.is_in_input_region(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
|
||||||
|
let activation_region = Rectangle::from_loc_and_size((0, 0), self.tile_size());
|
||||||
|
activation_region.to_f64().contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>) {
|
||||||
|
// Can't go through effective_border_width() because we might be fullscreen.
|
||||||
|
if !self.border.is_off() {
|
||||||
|
let width = self.border.width();
|
||||||
|
size.w = max(1, size.w - width * 2);
|
||||||
|
size.h = max(1, size.h - width * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window.request_size(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
|
||||||
|
if self.border.is_off() {
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
size.saturating_add(self.border.width() * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tile_height_for_window_height(&self, size: i32) -> i32 {
|
||||||
|
if self.border.is_off() {
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
size.saturating_add(self.border.width() * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_height_for_tile_height(&self, size: i32) -> i32 {
|
||||||
|
if self.border.is_off() {
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
size.saturating_sub(self.border.width() * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_fullscreen(&mut self, size: Size<i32, Logical>) {
|
||||||
|
self.fullscreen_backdrop.resize(size);
|
||||||
|
self.fullscreen_size = size;
|
||||||
|
self.window.request_fullscreen(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_size(&self) -> Size<i32, Logical> {
|
||||||
|
let mut size = self.window.min_size();
|
||||||
|
|
||||||
|
if let Some(width) = self.effective_border_width() {
|
||||||
|
size.w = max(1, size.w);
|
||||||
|
size.h = max(1, size.h);
|
||||||
|
|
||||||
|
size.w = size.w.saturating_add(width * 2);
|
||||||
|
size.h = size.h.saturating_add(width * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_size(&self) -> Size<i32, Logical> {
|
||||||
|
let mut size = self.window.max_size();
|
||||||
|
|
||||||
|
if let Some(width) = self.effective_border_width() {
|
||||||
|
if size.w > 0 {
|
||||||
|
size.w = size.w.saturating_add(width * 2);
|
||||||
|
}
|
||||||
|
if size.h > 0 {
|
||||||
|
size.h = size.h.saturating_add(width * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_ssd(&self) -> bool {
|
||||||
|
self.effective_border_width().is_some() || self.window.has_ssd()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<R: Renderer + ImportAll>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
) -> Vec<WorkspaceRenderElement<R>>
|
||||||
|
where
|
||||||
|
<R as Renderer>::TextureId: 'static,
|
||||||
|
{
|
||||||
|
let mut rv = Vec::new();
|
||||||
|
|
||||||
|
let window_pos = location + self.window_loc();
|
||||||
|
rv.extend(self.window.render(renderer, window_pos, scale));
|
||||||
|
|
||||||
|
if self.effective_border_width().is_some() {
|
||||||
|
rv.extend(
|
||||||
|
self.border
|
||||||
|
.render(scale)
|
||||||
|
.map(|elem| {
|
||||||
|
RelocateRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
Relocate::Relative,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(Into::into),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_fullscreen {
|
||||||
|
let elem = SolidColorRenderElement::from_buffer(
|
||||||
|
&self.fullscreen_backdrop,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
1.,
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
rv.push(elem.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
+118
-22
@@ -3,15 +3,19 @@ extern crate tracing;
|
|||||||
|
|
||||||
mod animation;
|
mod animation;
|
||||||
mod backend;
|
mod backend;
|
||||||
mod config;
|
mod config_error_notification;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
mod dbus;
|
mod dbus;
|
||||||
|
mod exit_confirm_dialog;
|
||||||
mod frame_clock;
|
mod frame_clock;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
|
mod hotkey_overlay;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod ipc;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod niri;
|
mod niri;
|
||||||
|
mod render_helpers;
|
||||||
mod screenshot_ui;
|
mod screenshot_ui;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod watcher;
|
mod watcher;
|
||||||
@@ -26,13 +30,13 @@ use std::path::PathBuf;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::{env, mem};
|
use std::{env, mem};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
use config::Config;
|
use directories::ProjectDirs;
|
||||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||||
use dummy_pw_utils as pw_utils;
|
use dummy_pw_utils as pw_utils;
|
||||||
use git_version::git_version;
|
use git_version::git_version;
|
||||||
use miette::{Context, NarratableReportHandler};
|
|
||||||
use niri::{Niri, State};
|
use niri::{Niri, State};
|
||||||
|
use niri_config::Config;
|
||||||
use portable_atomic::Ordering;
|
use portable_atomic::Ordering;
|
||||||
use sd_notify::NotifyState;
|
use sd_notify::NotifyState;
|
||||||
use smithay::reexports::calloop::{self, EventLoop};
|
use smithay::reexports::calloop::{self, EventLoop};
|
||||||
@@ -41,10 +45,14 @@ use tracing_subscriber::EnvFilter;
|
|||||||
use utils::spawn;
|
use utils::spawn;
|
||||||
use watcher::Watcher;
|
use watcher::Watcher;
|
||||||
|
|
||||||
use crate::utils::{REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
|
use crate::ipc::client::handle_msg;
|
||||||
|
use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version = version(), about, long_about = None)]
|
||||||
|
#[command(args_conflicts_with_subcommands = true)]
|
||||||
|
#[command(subcommand_value_name = "SUBCOMMAND")]
|
||||||
|
#[command(subcommand_help_heading = "Subcommands")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
@@ -52,9 +60,38 @@ struct Cli {
|
|||||||
/// Command to run upon compositor startup.
|
/// Command to run upon compositor startup.
|
||||||
#[arg(last = true)]
|
#[arg(last = true)]
|
||||||
command: Vec<OsString>,
|
command: Vec<OsString>,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommand: Option<Sub>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
#[derive(Subcommand)]
|
||||||
|
enum Sub {
|
||||||
|
/// Validate the config file.
|
||||||
|
Validate {
|
||||||
|
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Communicate with the running niri instance.
|
||||||
|
Msg {
|
||||||
|
#[command(subcommand)]
|
||||||
|
msg: Msg,
|
||||||
|
/// Format output as JSON.
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
|
/// Cause a panic to check if the backtraces are good.
|
||||||
|
Panic,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Msg {
|
||||||
|
/// List connected outputs.
|
||||||
|
Outputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Set backtrace defaults if not set.
|
// Set backtrace defaults if not set.
|
||||||
if env::var_os("RUST_BACKTRACE").is_none() {
|
if env::var_os("RUST_BACKTRACE").is_none() {
|
||||||
env::set_var("RUST_BACKTRACE", "1");
|
env::set_var("RUST_BACKTRACE", "1");
|
||||||
@@ -92,21 +129,46 @@ fn main() {
|
|||||||
|
|
||||||
let _client = tracy_client::Client::start();
|
let _client = tracy_client::Client::start();
|
||||||
|
|
||||||
info!(
|
// Set a better error printer for config loading.
|
||||||
"starting version {} ({})",
|
niri_config::set_miette_hook().unwrap();
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
git_version!(fallback = "unknown commit"),
|
// Handle subcommands.
|
||||||
);
|
if let Some(subcommand) = cli.subcommand {
|
||||||
|
match subcommand {
|
||||||
|
Sub::Validate { config } => {
|
||||||
|
let path = config
|
||||||
|
.or_else(default_config_path)
|
||||||
|
.expect("error getting config path");
|
||||||
|
Config::load(&path)?;
|
||||||
|
info!("config is valid");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Sub::Msg { msg, json } => {
|
||||||
|
handle_msg(msg, json)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Sub::Panic => cause_panic(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("starting version {}", &version());
|
||||||
|
|
||||||
// Load the config.
|
// Load the config.
|
||||||
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))).unwrap();
|
let path = cli.config.or_else(default_config_path);
|
||||||
let (mut config, path) = match Config::load(cli.config).context("error loading config") {
|
|
||||||
Ok((config, path)) => (config, Some(path)),
|
let mut config_errored = false;
|
||||||
Err(err) => {
|
let mut config = path
|
||||||
warn!("{err:?}");
|
.as_deref()
|
||||||
(Config::default(), None)
|
.and_then(|path| match Config::load(path) {
|
||||||
}
|
Ok(config) => Some(config),
|
||||||
};
|
Err(err) => {
|
||||||
|
warn!("{err:?}");
|
||||||
|
config_errored = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
|
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
|
||||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||||
|
|
||||||
@@ -128,14 +190,22 @@ fn main() {
|
|||||||
socket_name.to_string_lossy()
|
socket_name.to_string_lossy()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set NIRI_SOCKET for children.
|
||||||
|
if let Some(ipc) = &state.niri.ipc_server {
|
||||||
|
env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path);
|
||||||
|
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
if is_systemd_service {
|
if is_systemd_service {
|
||||||
// We're starting as a systemd service. Export our variables.
|
// We're starting as a systemd service. Export our variables.
|
||||||
import_env_to_systemd();
|
import_env_to_systemd();
|
||||||
|
|
||||||
// Inhibit power key handling so we can suspend on it.
|
// Inhibit power key handling so we can suspend on it.
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
if let Err(err) = state.niri.inhibit_power_key() {
|
if !state.niri.config.borrow().input.disable_power_key_handling {
|
||||||
warn!("error inhibiting power key: {err:?}");
|
if let Err(err) = state.niri.inhibit_power_key() {
|
||||||
|
warn!("error inhibiting power key: {err:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,10 +240,25 @@ fn main() {
|
|||||||
spawn(elem.command);
|
spawn(elem.command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show the config error notification right away if needed.
|
||||||
|
if config_errored {
|
||||||
|
state.niri.config_error_notification.show();
|
||||||
|
}
|
||||||
|
|
||||||
// Run the compositor.
|
// Run the compositor.
|
||||||
event_loop
|
event_loop
|
||||||
.run(None, &mut state, |state| state.refresh_and_flush_clients())
|
.run(None, &mut state, |state| state.refresh_and_flush_clients())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version() -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
git_version!(fallback = "unknown commit"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_env_to_systemd() {
|
fn import_env_to_systemd() {
|
||||||
@@ -203,3 +288,14 @@ fn import_env_to_systemd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_config_path() -> Option<PathBuf> {
|
||||||
|
let Some(dirs) = ProjectDirs::from("", "", "niri") else {
|
||||||
|
warn!("error retrieving home directory");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut path = dirs.config_dir().to_owned();
|
||||||
|
path.push("config.kdl");
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
|||||||
+1026
-269
File diff suppressed because it is too large
Load Diff
+11
-1
@@ -95,11 +95,20 @@ impl PipeWire {
|
|||||||
) -> anyhow::Result<Cast> {
|
) -> anyhow::Result<Cast> {
|
||||||
let _span = tracy_client::span!("PipeWire::start_cast");
|
let _span = tracy_client::span!("PipeWire::start_cast");
|
||||||
|
|
||||||
|
let to_niri_ = to_niri.clone();
|
||||||
let stop_cast = move || {
|
let stop_cast = move || {
|
||||||
if let Err(err) = to_niri.send(ScreenCastToNiri::StopCast { session_id }) {
|
if let Err(err) = to_niri_.send(ScreenCastToNiri::StopCast { session_id }) {
|
||||||
warn!("error sending StopCast to niri: {err:?}");
|
warn!("error sending StopCast to niri: {err:?}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let weak = output.downgrade();
|
||||||
|
let redraw = move || {
|
||||||
|
if let Some(output) = weak.upgrade() {
|
||||||
|
if let Err(err) = to_niri.send(ScreenCastToNiri::Redraw(output)) {
|
||||||
|
warn!("error sending Redraw to niri: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mode = output.current_mode().unwrap();
|
let mode = output.current_mode().unwrap();
|
||||||
let size = mode.size;
|
let size = mode.size;
|
||||||
@@ -158,6 +167,7 @@ impl PipeWire {
|
|||||||
StreamState::Connecting => (),
|
StreamState::Connecting => (),
|
||||||
StreamState::Streaming => {
|
StreamState::Streaming => {
|
||||||
is_active.set(true);
|
is_active.set(true);
|
||||||
|
redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
|
use smithay::backend::renderer::element::texture::TextureRenderElement;
|
||||||
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||||
|
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||||
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
|
use smithay::backend::renderer::{
|
||||||
|
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
||||||
|
};
|
||||||
|
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
|
/// Trait with our main renderer requirements to save on the typing.
|
||||||
|
pub trait NiriRenderer:
|
||||||
|
ImportAll
|
||||||
|
+ ImportMem
|
||||||
|
+ ExportMem
|
||||||
|
+ Bind<Dmabuf>
|
||||||
|
+ Offscreen<GlesTexture>
|
||||||
|
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
|
||||||
|
+ AsGlesRenderer
|
||||||
|
{
|
||||||
|
// Associated types to work around the instability of associated type bounds.
|
||||||
|
type NiriTextureId: Texture + Clone + 'static;
|
||||||
|
type NiriError: std::error::Error
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ From<<GlesRenderer as Renderer>::Error>
|
||||||
|
+ 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> NiriRenderer for R
|
||||||
|
where
|
||||||
|
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||||
|
R::TextureId: Texture + Clone + 'static,
|
||||||
|
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||||
|
{
|
||||||
|
type NiriTextureId = R::TextureId;
|
||||||
|
type NiriError = R::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for getting the underlying `GlesRenderer`.
|
||||||
|
pub trait AsGlesRenderer {
|
||||||
|
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsGlesRenderer for GlesRenderer {
|
||||||
|
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
|
||||||
|
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||||
|
self.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for getting the underlying `GlesFrame`.
|
||||||
|
pub trait AsGlesFrame<'frame>
|
||||||
|
where
|
||||||
|
Self: 'frame,
|
||||||
|
{
|
||||||
|
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||||
|
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
|
||||||
|
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||||
|
self.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
|
||||||
|
|
||||||
|
impl Element for PrimaryGpuTextureRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
self.0.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.0.current_commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.0.geometry(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> Transform {
|
||||||
|
self.0.transform()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
self.0.src()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
commit: Option<CommitCounter>,
|
||||||
|
) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
self.0.damage_since(scale, commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
self.0.opaque_regions(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
self.0.alpha()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> Kind {
|
||||||
|
self.0.kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
let gles_frame = frame.as_gles_frame();
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||||
|
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||||
|
// the target GPU into account.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
|
||||||
|
for PrimaryGpuTextureRenderElement
|
||||||
|
{
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut TtyFrame<'_, '_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), TtyRendererError<'render, 'alloc>> {
|
||||||
|
let gles_frame = frame.as_gles_frame();
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(
|
||||||
|
&self,
|
||||||
|
_renderer: &mut TtyRenderer<'render, 'alloc>,
|
||||||
|
) -> Option<UnderlyingStorage> {
|
||||||
|
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||||
|
// the target GPU into account.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
+156
-19
@@ -5,19 +5,21 @@ use std::mem;
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use arrayvec::ArrayVec;
|
use arrayvec::ArrayVec;
|
||||||
|
use niri_config::Action;
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::input::{ButtonState, MouseButton};
|
use smithay::backend::input::{ButtonState, MouseButton};
|
||||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||||
use smithay::backend::renderer::element::Kind;
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||||
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
use smithay::backend::renderer::ExportMem;
|
use smithay::backend::renderer::ExportMem;
|
||||||
use smithay::input::keyboard::{Keysym, ModifiersState};
|
use smithay::input::keyboard::{Keysym, ModifiersState};
|
||||||
use smithay::output::{Output, WeakOutput};
|
use smithay::output::{Output, WeakOutput};
|
||||||
use smithay::render_elements;
|
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||||
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
|
|
||||||
|
|
||||||
use crate::config::Action;
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
use crate::render_helpers::PrimaryGpuTextureRenderElement;
|
||||||
|
|
||||||
const BORDER: i32 = 2;
|
const BORDER: i32 = 2;
|
||||||
|
|
||||||
@@ -39,17 +41,17 @@ pub enum ScreenshotUi {
|
|||||||
|
|
||||||
pub struct OutputData {
|
pub struct OutputData {
|
||||||
size: Size<i32, Physical>,
|
size: Size<i32, Physical>,
|
||||||
|
scale: i32,
|
||||||
texture: GlesTexture,
|
texture: GlesTexture,
|
||||||
texture_buffer: TextureBuffer<GlesTexture>,
|
texture_buffer: TextureBuffer<GlesTexture>,
|
||||||
buffers: [SolidColorBuffer; 8],
|
buffers: [SolidColorBuffer; 8],
|
||||||
locations: [Point<i32, Physical>; 8],
|
locations: [Point<i32, Physical>; 8],
|
||||||
}
|
}
|
||||||
|
|
||||||
render_elements! {
|
#[derive(Debug)]
|
||||||
#[derive(Debug)]
|
pub enum ScreenshotUiRenderElement {
|
||||||
pub ScreenshotUiRenderElement<R>;
|
Screenshot(PrimaryGpuTextureRenderElement),
|
||||||
Screenshot = TextureRenderElement<R::TextureId>,
|
SolidColor(SolidColorRenderElement),
|
||||||
SolidColor = SolidColorRenderElement,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScreenshotUi {
|
impl ScreenshotUi {
|
||||||
@@ -105,10 +107,11 @@ impl ScreenshotUi {
|
|||||||
let output_transform = output.current_transform();
|
let output_transform = output.current_transform();
|
||||||
let output_mode = output.current_mode().unwrap();
|
let output_mode = output.current_mode().unwrap();
|
||||||
let size = output_transform.transform_size(output_mode.size);
|
let size = output_transform.transform_size(output_mode.size);
|
||||||
|
let scale = output.current_scale().integer_scale();
|
||||||
let texture_buffer = TextureBuffer::from_texture(
|
let texture_buffer = TextureBuffer::from_texture(
|
||||||
renderer,
|
renderer,
|
||||||
texture.clone(),
|
texture.clone(),
|
||||||
output.current_scale().integer_scale(),
|
scale,
|
||||||
Transform::Normal,
|
Transform::Normal,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@@ -125,6 +128,7 @@ impl ScreenshotUi {
|
|||||||
let locations = [Default::default(); 8];
|
let locations = [Default::default(); 8];
|
||||||
let data = OutputData {
|
let data = OutputData {
|
||||||
size,
|
size,
|
||||||
|
scale,
|
||||||
texture,
|
texture,
|
||||||
texture_buffer,
|
texture_buffer,
|
||||||
buffers,
|
buffers,
|
||||||
@@ -236,10 +240,7 @@ impl ScreenshotUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_output(
|
pub fn render_output(&self, output: &Output) -> ArrayVec<ScreenshotUiRenderElement, 9> {
|
||||||
&self,
|
|
||||||
output: &Output,
|
|
||||||
) -> ArrayVec<ScreenshotUiRenderElement<GlesRenderer>, 9> {
|
|
||||||
let _span = tracy_client::span!("ScreenshotUi::render_output");
|
let _span = tracy_client::span!("ScreenshotUi::render_output");
|
||||||
|
|
||||||
let Self::Open { output_data, .. } = self else {
|
let Self::Open { output_data, .. } = self else {
|
||||||
@@ -266,14 +267,14 @@ impl ScreenshotUi {
|
|||||||
|
|
||||||
// The screenshot itself goes last.
|
// The screenshot itself goes last.
|
||||||
elements.push(
|
elements.push(
|
||||||
TextureRenderElement::from_texture_buffer(
|
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||||
(0., 0.),
|
(0., 0.),
|
||||||
&output_data.texture_buffer,
|
&output_data.texture_buffer,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Kind::Unspecified,
|
Kind::Unspecified,
|
||||||
)
|
))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -332,9 +333,10 @@ impl ScreenshotUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn output_size(&self, output: &Output) -> Option<Size<i32, Physical>> {
|
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32)> {
|
||||||
if let Self::Open { output_data, .. } = self {
|
if let Self::Open { output_data, .. } = self {
|
||||||
Some(output_data.get(output)?.size)
|
let data = output_data.get(output)?;
|
||||||
|
Some((data.size, data.scale))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -446,3 +448,138 @@ pub fn rect_from_corner_points(
|
|||||||
let y2 = max(a.y, b.y);
|
let y2 = max(a.y, b.y);
|
||||||
Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale))
|
Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manual RenderElement implementation due to AsGlesFrame requirement.
|
||||||
|
impl Element for ScreenshotUiRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.id(),
|
||||||
|
Self::SolidColor(elem) => elem.id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.current_commit(),
|
||||||
|
Self::SolidColor(elem) => elem.current_commit(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.geometry(scale),
|
||||||
|
Self::SolidColor(elem) => elem.geometry(scale),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> Transform {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.transform(),
|
||||||
|
Self::SolidColor(elem) => elem.transform(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.src(),
|
||||||
|
Self::SolidColor(elem) => elem.src(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
commit: Option<CommitCounter>,
|
||||||
|
) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.damage_since(scale, commit),
|
||||||
|
Self::SolidColor(elem) => elem.damage_since(scale, commit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.opaque_regions(scale),
|
||||||
|
Self::SolidColor(elem) => elem.opaque_regions(scale),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.alpha(),
|
||||||
|
Self::SolidColor(elem) => elem.alpha(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> Kind {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => elem.kind(),
|
||||||
|
Self::SolidColor(elem) => elem.kind(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for ScreenshotUiRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => {
|
||||||
|
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
||||||
|
}
|
||||||
|
Self::SolidColor(elem) => {
|
||||||
|
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||||
|
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||||
|
// the target GPU into account.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>> for ScreenshotUiRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut TtyFrame<'render, 'alloc, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), TtyRendererError<'render, 'alloc>> {
|
||||||
|
match self {
|
||||||
|
Self::Screenshot(elem) => {
|
||||||
|
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
||||||
|
}
|
||||||
|
Self::SolidColor(elem) => {
|
||||||
|
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(
|
||||||
|
&self,
|
||||||
|
_renderer: &mut TtyRenderer<'render, 'alloc>,
|
||||||
|
) -> Option<UnderlyingStorage> {
|
||||||
|
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||||
|
// the target GPU into account.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SolidColorRenderElement> for ScreenshotUiRenderElement {
|
||||||
|
fn from(x: SolidColorRenderElement) -> Self {
|
||||||
|
Self::SolidColor(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PrimaryGpuTextureRenderElement> for ScreenshotUiRenderElement {
|
||||||
|
fn from(x: PrimaryGpuTextureRenderElement) -> Self {
|
||||||
|
Self::Screenshot(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+23
-2
@@ -11,10 +11,14 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{ensure, Context};
|
use anyhow::{ensure, Context};
|
||||||
use directories::UserDirs;
|
use directories::UserDirs;
|
||||||
|
use niri_config::Config;
|
||||||
|
use smithay::output::Output;
|
||||||
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
||||||
use smithay::utils::{Logical, Point, Rectangle};
|
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use crate::config::Config;
|
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
|
||||||
|
(t.0.clone(), t.1.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_monotonic_time() -> Duration {
|
pub fn get_monotonic_time() -> Duration {
|
||||||
let ts = clock_gettime(ClockId::Monotonic);
|
let ts = clock_gettime(ClockId::Monotonic);
|
||||||
@@ -25,6 +29,16 @@ pub fn center(rect: Rectangle<i32, Logical>) -> Point<i32, Logical> {
|
|||||||
rect.loc + rect.size.downscale(2).to_point()
|
rect.loc + rect.size.downscale(2).to_point()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn output_size(output: &Output) -> Size<i32, Logical> {
|
||||||
|
let output_scale = output.current_scale().integer_scale();
|
||||||
|
let output_transform = output.current_transform();
|
||||||
|
let output_mode = output.current_mode().unwrap();
|
||||||
|
|
||||||
|
output_transform
|
||||||
|
.transform_size(output_mode.size)
|
||||||
|
.to_logical(output_scale)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
|
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
|
||||||
let Some(path) = &config.screenshot_path else {
|
let Some(path) = &config.screenshot_path else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -176,3 +190,10 @@ pub fn show_screenshot_notification(image_path: Option<PathBuf>) {
|
|||||||
warn!("error showing screenshot notification: {err:?}");
|
warn!("error showing screenshot notification: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
pub fn cause_panic() {
|
||||||
|
let a = Duration::from_secs(1);
|
||||||
|
let b = Duration::from_secs(2);
|
||||||
|
let _ = a - b;
|
||||||
|
}
|
||||||
|
|||||||
+18
-4
@@ -27,7 +27,18 @@ impl Watcher {
|
|||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
|
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
|
// this "should" be as simple as mtime, but it does not quite work in practice;
|
||||||
|
// it doesn't work if the config is a symlink, and its target changes but the
|
||||||
|
// new target and old target have identical mtimes.
|
||||||
|
//
|
||||||
|
// in practice, this does not occur on any systems other than nix.
|
||||||
|
// because, on nix practically everything is a symlink to /nix/store
|
||||||
|
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
|
||||||
|
// so, symlink targets change frequently when mtime doesn't.
|
||||||
|
let mut last_props = path
|
||||||
|
.canonicalize()
|
||||||
|
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||||
|
.ok();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
thread::sleep(Duration::from_millis(500));
|
thread::sleep(Duration::from_millis(500));
|
||||||
@@ -36,8 +47,11 @@ impl Watcher {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
|
if let Ok(new_props) = path
|
||||||
if last_mtime != Some(mtime) {
|
.canonicalize()
|
||||||
|
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||||
|
{
|
||||||
|
if last_props.as_ref() != Some(&new_props) {
|
||||||
trace!("file changed: {}", path.to_string_lossy());
|
trace!("file changed: {}", path.to_string_lossy());
|
||||||
|
|
||||||
if let Err(err) = changed.send(()) {
|
if let Err(err) = changed.send(()) {
|
||||||
@@ -45,7 +59,7 @@ impl Watcher {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_mtime = Some(mtime);
|
last_props = Some(new_props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user