mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
+376
-133
File diff suppressed because it is too large
Load Diff
+49
-22
@@ -1,13 +1,37 @@
|
|||||||
[package]
|
[workspace.package]
|
||||||
name = "niri"
|
version = "0.1.0-beta.1"
|
||||||
version = "0.1.0-alpha.2"
|
|
||||||
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"
|
||||||
|
repository = "https://github.com/YaLTeR/niri"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
bitflags = "2.4.2"
|
||||||
|
directories = "5.0.1"
|
||||||
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
|
tracy-client = { version = "0.16.5", default-features = false }
|
||||||
|
|
||||||
|
[workspace.dependencies.smithay]
|
||||||
|
git = "https://github.com/Smithay/smithay.git"
|
||||||
|
# path = "../smithay"
|
||||||
|
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"
|
readme = "README.md"
|
||||||
repository = "https://github.com/YaLTeR/niri"
|
|
||||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -15,34 +39,37 @@ anyhow = { version = "1.0.79" }
|
|||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
async-channel = { version = "2.1.1", optional = true }
|
async-channel = { version = "2.1.1", optional = true }
|
||||||
async-io = { version = "1.13.0", optional = true }
|
async-io = { version = "1.13.0", optional = true }
|
||||||
bitflags = "2.4.1"
|
bitflags = "2.4.2"
|
||||||
clap = { version = "4.4.13", features = ["derive"] }
|
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
|
||||||
|
clap = { version = "4.4.18", features = ["derive", "string"] }
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
|
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||||
git-version = "0.3.9"
|
git-version = "0.3.9"
|
||||||
keyframe = { version = "1.1.1", default-features = false }
|
keyframe = { version = "1.1.1", default-features = false }
|
||||||
knuffel = "3.2.0"
|
libc = "0.2.152"
|
||||||
libc = "0.2.151"
|
|
||||||
logind-zbus = { version = "3.1.2", optional = true }
|
|
||||||
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
miette = "5.10.0"
|
logind-zbus = { version = "3.1.2", optional = true }
|
||||||
|
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
|
||||||
|
niri-ipc = { version = "0.1.0-beta.1", path = "niri-ipc" }
|
||||||
notify-rust = { version = "4.10.0", optional = true }
|
notify-rust = { version = "4.10.0", optional = true }
|
||||||
|
pangocairo = "0.18.0"
|
||||||
pipewire = { version = "0.7.2", optional = true }
|
pipewire = { version = "0.7.2", optional = true }
|
||||||
png = "0.17.10"
|
png = "0.17.11"
|
||||||
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||||
profiling = "1.0.13"
|
profiling = "1.0.13"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
serde.workspace = true
|
||||||
|
serde_json = "1.0.111"
|
||||||
|
smithay-drm-extras.workspace = true
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
tracing.workspace = true
|
||||||
tracy-client = { version = "0.16.5", default-features = false }
|
tracy-client.workspace = true
|
||||||
url = { version = "2.5.0", optional = true }
|
url = { version = "2.5.0", optional = true }
|
||||||
xcursor = "0.3.5"
|
xcursor = "0.3.5"
|
||||||
zbus = { version = "3.14.1", optional = true }
|
zbus = { version = "3.14.1", optional = true }
|
||||||
|
|
||||||
[dependencies.smithay]
|
[dependencies.smithay]
|
||||||
git = "https://github.com/Smithay/smithay.git"
|
workspace = true
|
||||||
# path = "../smithay"
|
|
||||||
default-features = false
|
|
||||||
features = [
|
features = [
|
||||||
"backend_drm",
|
"backend_drm",
|
||||||
"backend_egl",
|
"backend_egl",
|
||||||
@@ -58,10 +85,6 @@ features = [
|
|||||||
"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"
|
||||||
@@ -80,8 +103,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~alpha.2"
|
version = "0.1.0~beta.1"
|
||||||
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,8 +1,33 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
@@ -12,21 +37,12 @@ Have your waybars and fuzzels ready: niri is not a complete desktop environment.
|
|||||||
|
|
||||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||||
|
|
||||||
## 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.
|
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.
|
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
||||||
@@ -36,7 +52,7 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
|||||||
> [!TIP]
|
> [!TIP]
|
||||||
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
> 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 NixOS users, check out https://github.com/sodiboo/niri-flake
|
||||||
|
|
||||||
First, install the dependencies for your distribution.
|
First, install the dependencies for your distribution.
|
||||||
|
|
||||||
@@ -46,17 +62,27 @@ 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, 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
|
||||||
|
|
||||||
The recommended way to install and run niri is as a standalone desktop session.
|
The recommended way to install and run niri is as a standalone desktop session.
|
||||||
@@ -94,10 +120,23 @@ A step-by-step process for this is explained [on the wiki](https://github.com/Ya
|
|||||||
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
|
### Xwayland
|
||||||
|
|
||||||
See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri.
|
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>.
|
||||||
@@ -107,6 +146,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) |
|
||||||
@@ -122,13 +162,13 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
|||||||
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last 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>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 |
|
||||||
@@ -153,9 +193,11 @@ 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
|
||||||
|
|||||||
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, XkbConfig};
|
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,21 +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 = default_border())]
|
|
||||||
pub border: FocusRing,
|
|
||||||
#[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(child, default)]
|
|
||||||
pub struts: Struts,
|
|
||||||
#[knuffel(
|
#[knuffel(
|
||||||
child,
|
child,
|
||||||
unwrap(argument),
|
unwrap(argument),
|
||||||
@@ -41,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,
|
||||||
@@ -54,6 +49,8 @@ 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)]
|
#[knuffel(child)]
|
||||||
pub disable_power_key_handling: bool,
|
pub disable_power_key_handling: bool,
|
||||||
@@ -98,6 +95,18 @@ impl Xkb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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.
|
||||||
@@ -113,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)]
|
||||||
@@ -150,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,
|
||||||
@@ -158,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)]
|
||||||
@@ -265,6 +338,12 @@ pub struct Struts {
|
|||||||
pub bottom: u16,
|
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>);
|
||||||
|
|
||||||
@@ -336,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,
|
||||||
@@ -346,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)]
|
||||||
@@ -400,28 +487,23 @@ impl Default for DebugConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,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()
|
||||||
}
|
}
|
||||||
@@ -558,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;
|
||||||
@@ -591,7 +705,16 @@ 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 {
|
||||||
@@ -607,8 +730,7 @@ mod tests {
|
|||||||
mode "1920x1080@144"
|
mode "1920x1080@144"
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn-at-startup "alacritty" "-e" "fish"
|
layout {
|
||||||
|
|
||||||
focus-ring {
|
focus-ring {
|
||||||
width 5
|
width 5
|
||||||
active-color 0 100 200 255
|
active-color 0 100 200 255
|
||||||
@@ -621,13 +743,6 @@ mod tests {
|
|||||||
inactive-color 255 200 100 0
|
inactive-color 255 200 100 0
|
||||||
}
|
}
|
||||||
|
|
||||||
prefer-no-csd
|
|
||||||
|
|
||||||
cursor {
|
|
||||||
xcursor-theme "breeze_cursors"
|
|
||||||
xcursor-size 16
|
|
||||||
}
|
|
||||||
|
|
||||||
preset-column-widths {
|
preset-column-widths {
|
||||||
proportion 0.25
|
proportion 0.25
|
||||||
proportion 0.5
|
proportion 0.5
|
||||||
@@ -645,8 +760,24 @@ mod tests {
|
|||||||
top 3
|
top 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
center-focused-column "on-overflow"
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn-at-startup "alacritty" "-e" "fish"
|
||||||
|
|
||||||
|
prefer-no-csd
|
||||||
|
|
||||||
|
cursor {
|
||||||
|
xcursor-theme "breeze_cursors"
|
||||||
|
xcursor-size 16
|
||||||
|
}
|
||||||
|
|
||||||
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; }
|
||||||
@@ -675,8 +806,16 @@ 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()),
|
||||||
@@ -694,9 +833,7 @@ mod tests {
|
|||||||
refresh: Some(144.),
|
refresh: Some(144.),
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
spawn_at_startup: vec![SpawnAtStartup {
|
layout: Layout {
|
||||||
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
|
|
||||||
}],
|
|
||||||
focus_ring: FocusRing {
|
focus_ring: FocusRing {
|
||||||
off: false,
|
off: false,
|
||||||
width: 5,
|
width: 5,
|
||||||
@@ -729,18 +866,15 @@ mod tests {
|
|||||||
a: 0,
|
a: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
prefer_no_csd: true,
|
|
||||||
cursor: Cursor {
|
|
||||||
xcursor_theme: String::from("breeze_cursors"),
|
|
||||||
xcursor_size: 16,
|
|
||||||
},
|
|
||||||
preset_column_widths: vec![
|
preset_column_widths: vec![
|
||||||
PresetWidth::Proportion(0.25),
|
PresetWidth::Proportion(0.25),
|
||||||
PresetWidth::Proportion(0.5),
|
PresetWidth::Proportion(0.5),
|
||||||
PresetWidth::Fixed(960),
|
PresetWidth::Fixed(960),
|
||||||
PresetWidth::Fixed(1280),
|
PresetWidth::Fixed(1280),
|
||||||
],
|
],
|
||||||
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(0.25)])),
|
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(
|
||||||
|
0.25,
|
||||||
|
)])),
|
||||||
gaps: 8,
|
gaps: 8,
|
||||||
struts: Struts {
|
struts: Struts {
|
||||||
left: 1,
|
left: 1,
|
||||||
@@ -748,7 +882,20 @@ mod tests {
|
|||||||
top: 3,
|
top: 3,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
|
center_focused_column: CenterFocusedColumn::OnOverflow,
|
||||||
|
},
|
||||||
|
spawn_at_startup: vec![SpawnAtStartup {
|
||||||
|
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
|
||||||
|
}],
|
||||||
|
prefer_no_csd: true,
|
||||||
|
cursor: Cursor {
|
||||||
|
xcursor_theme: String::from("breeze_cursors"),
|
||||||
|
xcursor_size: 16,
|
||||||
|
},
|
||||||
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 {
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
+127
-56
@@ -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 {
|
||||||
@@ -45,7 +54,8 @@ input {
|
|||||||
// disable-power-key-handling
|
// 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" {
|
||||||
@@ -60,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.
|
||||||
@@ -75,11 +85,7 @@ input {
|
|||||||
position x=1280 y=0
|
position x=1280 y=0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add lines like this to spawn processes at startup.
|
layout {
|
||||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
|
||||||
// which may be more convenient to use.
|
|
||||||
// spawn-at-startup "alacritty" "-e" "fish"
|
|
||||||
|
|
||||||
// You can change how the focus ring looks.
|
// You can change how the focus ring looks.
|
||||||
focus-ring {
|
focus-ring {
|
||||||
// Uncomment this line to disable the focus ring.
|
// Uncomment this line to disable the focus ring.
|
||||||
@@ -106,18 +112,6 @@ border {
|
|||||||
inactive-color 80 80 80 255
|
inactive-color 80 80 80 255
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor {
|
|
||||||
// Change the theme and size of the cursor as well as set the
|
|
||||||
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
|
|
||||||
// xcursor-theme "default"
|
|
||||||
// xcursor-size 24
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
|
||||||
// If the client will specifically ask for CSD, the request will be honored.
|
|
||||||
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
|
||||||
// prefer-no-csd
|
|
||||||
|
|
||||||
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||||
preset-column-widths {
|
preset-column-widths {
|
||||||
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
||||||
@@ -151,6 +145,32 @@ struts {
|
|||||||
// bottom 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.
|
||||||
|
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||||
|
// which may be more convenient to use.
|
||||||
|
// spawn-at-startup "alacritty" "-e" "fish"
|
||||||
|
|
||||||
|
cursor {
|
||||||
|
// Change the theme and size of the cursor as well as set the
|
||||||
|
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
|
||||||
|
// xcursor-theme "default"
|
||||||
|
// xcursor-size 24
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
||||||
|
// If the client will specifically ask for CSD, the request will be honored.
|
||||||
|
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||||
|
// prefer-no-csd
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -159,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
|
||||||
@@ -167,34 +193,41 @@ 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
|
// Alternative commands that move across workspaces when reaching
|
||||||
// the first or last window in a column.
|
// the first or last window in a column.
|
||||||
@@ -208,37 +241,45 @@ binds {
|
|||||||
Mod+Ctrl+Home { move-column-to-first; }
|
Mod+Ctrl+Home { move-column-to-first; }
|
||||||
Mod+Ctrl+End { move-column-to-last; }
|
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; }
|
||||||
@@ -249,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; }
|
||||||
@@ -299,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -1,4 +1,6 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
@@ -110,11 +112,18 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(feature = "dbus"), allow(unused))]
|
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
|
||||||
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.connectors(),
|
Backend::Tty(tty) => tty.ipc_outputs(),
|
||||||
Backend::Winit(winit) => winit.connectors(),
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,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
|
||||||
|
|||||||
+293
-93
@@ -9,6 +9,7 @@ use std::{io, mem};
|
|||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use libc::dev_t;
|
use libc::dev_t;
|
||||||
|
use niri_config::Config;
|
||||||
use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufAllocator};
|
use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufAllocator};
|
||||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||||
use smithay::backend::allocator::{Format, Fourcc};
|
use smithay::backend::allocator::{Format, Fourcc};
|
||||||
@@ -27,11 +28,11 @@ use smithay::backend::session::libseat::LibSeatSession;
|
|||||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||||
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
|
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
|
||||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||||
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel};
|
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Subpixel};
|
||||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||||
use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken};
|
use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken};
|
||||||
use smithay::reexports::drm::control::{
|
use smithay::reexports::drm::control::{
|
||||||
connector, crtc, property, Device, Mode as DrmMode, ModeFlags, ModeTypeFlags,
|
self, connector, crtc, property, Device, Mode as DrmMode, ModeFlags, ModeTypeFlags,
|
||||||
};
|
};
|
||||||
use smithay::reexports::gbm::Modifier;
|
use smithay::reexports::gbm::Modifier;
|
||||||
use smithay::reexports::input::Libinput;
|
use smithay::reexports::input::Libinput;
|
||||||
@@ -46,7 +47,7 @@ use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_
|
|||||||
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||||
|
|
||||||
use super::RenderResult;
|
use super::RenderResult;
|
||||||
use crate::config::Config;
|
use crate::frame_clock::FrameClock;
|
||||||
use crate::niri::{RedrawState, State};
|
use crate::niri::{RedrawState, State};
|
||||||
use crate::render_helpers::AsGlesRenderer;
|
use crate::render_helpers::AsGlesRenderer;
|
||||||
use crate::utils::get_monotonic_time;
|
use crate::utils::get_monotonic_time;
|
||||||
@@ -73,7 +74,8 @@ pub struct Tty {
|
|||||||
// The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to
|
// The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to
|
||||||
// the primary GPU.
|
// the primary GPU.
|
||||||
primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
|
primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
|
||||||
connectors: Arc<Mutex<HashMap<String, Output>>>,
|
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
||||||
|
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type TtyRenderer<'render, 'alloc> = MultiRenderer<
|
pub type TtyRenderer<'render, 'alloc> = MultiRenderer<
|
||||||
@@ -220,7 +222,8 @@ impl Tty {
|
|||||||
devices: HashMap::new(),
|
devices: HashMap::new(),
|
||||||
dmabuf_global: None,
|
dmabuf_global: None,
|
||||||
primary_allocator: None,
|
primary_allocator: None,
|
||||||
connectors: Arc::new(Mutex::new(HashMap::new())),
|
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
|
||||||
|
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,10 +328,10 @@ impl Tty {
|
|||||||
let crtcs: Vec<_> = device
|
let crtcs: Vec<_> = device
|
||||||
.drm_scanner
|
.drm_scanner
|
||||||
.crtcs()
|
.crtcs()
|
||||||
.map(|(conn, crtc)| (conn.clone(), crtc))
|
.map(|(_conn, crtc)| crtc)
|
||||||
.collect();
|
.collect();
|
||||||
for (conn, crtc) in crtcs {
|
for crtc in crtcs {
|
||||||
self.connector_disconnected(niri, node, conn, crtc);
|
self.connector_disconnected(niri, node, crtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
let device = self.devices.get_mut(&node).unwrap();
|
let device = self.devices.get_mut(&node).unwrap();
|
||||||
@@ -366,6 +369,8 @@ impl Tty {
|
|||||||
warn!("error adding device: {err:?}");
|
warn!("error adding device: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.refresh_ipc_outputs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,12 +511,13 @@ impl Tty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DrmScanEvent::Disconnected {
|
DrmScanEvent::Disconnected {
|
||||||
connector,
|
crtc: Some(crtc), ..
|
||||||
crtc: Some(crtc),
|
} => self.connector_disconnected(niri, node, crtc),
|
||||||
} => self.connector_disconnected(niri, node, connector, crtc),
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.refresh_ipc_outputs();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
|
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
|
||||||
@@ -530,11 +536,11 @@ impl Tty {
|
|||||||
let crtcs: Vec<_> = device
|
let crtcs: Vec<_> = device
|
||||||
.drm_scanner
|
.drm_scanner
|
||||||
.crtcs()
|
.crtcs()
|
||||||
.map(|(info, crtc)| (info.clone(), crtc))
|
.map(|(_info, crtc)| crtc)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (connector, crtc) in crtcs {
|
for crtc in crtcs {
|
||||||
self.connector_disconnected(niri, node, connector, crtc);
|
self.connector_disconnected(niri, node, crtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
let device = self.devices.remove(&node).unwrap();
|
let device = self.devices.remove(&node).unwrap();
|
||||||
@@ -576,6 +582,8 @@ impl Tty {
|
|||||||
|
|
||||||
self.gpu_manager.as_mut().remove_node(&device.render_node);
|
self.gpu_manager.as_mut().remove_node(&device.render_node);
|
||||||
niri.event_loop.remove(device.token);
|
niri.event_loop.remove(device.token);
|
||||||
|
|
||||||
|
self.refresh_ipc_outputs();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connector_connected(
|
fn connector_connected(
|
||||||
@@ -608,46 +616,14 @@ impl Tty {
|
|||||||
|
|
||||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||||
|
|
||||||
// FIXME: print modes here until we have a better way to list all modes.
|
|
||||||
for m in connector.modes() {
|
for m in connector.modes() {
|
||||||
let wl_mode = Mode::from(*m);
|
|
||||||
debug!(
|
|
||||||
"mode: {}x{}@{:.3}",
|
|
||||||
m.size().0,
|
|
||||||
m.size().1,
|
|
||||||
wl_mode.refresh as f64 / 1000.,
|
|
||||||
);
|
|
||||||
|
|
||||||
trace!("{m:?}");
|
trace!("{m:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut mode = None;
|
let (mode, fallback) =
|
||||||
|
pick_mode(&connector, config.mode).ok_or_else(|| anyhow!("no mode"))?;
|
||||||
if let Some(target) = &config.mode {
|
if fallback {
|
||||||
let refresh = target.refresh.map(|r| (r * 1000.).round() as i32);
|
let target = config.mode.unwrap();
|
||||||
|
|
||||||
for m in connector.modes() {
|
|
||||||
if m.size() != (target.width, target.height) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(refresh) = refresh {
|
|
||||||
// If refresh is set, only pick modes with matching refresh.
|
|
||||||
let wl_mode = Mode::from(*m);
|
|
||||||
if wl_mode.refresh == refresh {
|
|
||||||
mode = Some(m);
|
|
||||||
}
|
|
||||||
} else if let Some(curr) = mode {
|
|
||||||
// If refresh isn't set, pick the mode with the highest refresh.
|
|
||||||
if curr.vrefresh() < m.vrefresh() {
|
|
||||||
mode = Some(m);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mode = Some(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode.is_none() {
|
|
||||||
warn!(
|
warn!(
|
||||||
"configured mode {}x{}{} could not be found, falling back to preferred",
|
"configured mode {}x{}{} could not be found, falling back to preferred",
|
||||||
target.width,
|
target.width,
|
||||||
@@ -659,36 +635,11 @@ impl Tty {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if mode.is_none() {
|
|
||||||
// Pick a preferred mode.
|
|
||||||
for m in connector.modes() {
|
|
||||||
if !m.mode_type().contains(ModeTypeFlags::PREFERRED) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(curr) = mode {
|
|
||||||
if curr.vrefresh() < m.vrefresh() {
|
|
||||||
mode = Some(m);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mode = Some(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode.is_none() {
|
|
||||||
// Last attempt.
|
|
||||||
mode = connector.modes().first();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mode = mode.ok_or_else(|| anyhow!("no mode"))?;
|
|
||||||
debug!("picking mode: {mode:?}");
|
debug!("picking mode: {mode:?}");
|
||||||
|
|
||||||
let surface = device
|
let surface = device
|
||||||
.drm
|
.drm
|
||||||
.create_surface(crtc, *mode, &[connector.handle()])?;
|
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||||
|
|
||||||
// Create GBM allocator.
|
// Create GBM allocator.
|
||||||
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
|
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
|
||||||
@@ -711,9 +662,8 @@ impl Tty {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let wl_mode = Mode::from(*mode);
|
let wl_mode = Mode::from(mode);
|
||||||
let scale = config.scale.clamp(1., 10.).ceil() as i32;
|
output.change_current_state(Some(wl_mode), None, None, None);
|
||||||
output.change_current_state(Some(wl_mode), None, Some(Scale::Integer(scale)), None);
|
|
||||||
output.set_preferred(wl_mode);
|
output.set_preferred(wl_mode);
|
||||||
|
|
||||||
output
|
output
|
||||||
@@ -785,7 +735,7 @@ impl Tty {
|
|||||||
let sequence_delta_plot_name =
|
let sequence_delta_plot_name =
|
||||||
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
|
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
|
||||||
|
|
||||||
self.connectors
|
self.enabled_outputs
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(output_name.clone(), output.clone());
|
.insert(output_name.clone(), output.clone());
|
||||||
@@ -803,7 +753,7 @@ impl Tty {
|
|||||||
let res = device.surfaces.insert(crtc, surface);
|
let res = device.surfaces.insert(crtc, surface);
|
||||||
assert!(res.is_none(), "crtc must not have already existed");
|
assert!(res.is_none(), "crtc must not have already existed");
|
||||||
|
|
||||||
niri.add_output(output.clone(), Some(refresh_interval(*mode)));
|
niri.add_output(output.clone(), Some(refresh_interval(mode)));
|
||||||
|
|
||||||
// Power on all monitors if necessary and queue a redraw on the new one.
|
// Power on all monitors if necessary and queue a redraw on the new one.
|
||||||
niri.event_loop.insert_idle(move |state| {
|
niri.event_loop.insert_idle(move |state| {
|
||||||
@@ -814,25 +764,21 @@ impl Tty {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connector_disconnected(
|
fn connector_disconnected(&mut self, niri: &mut Niri, node: DrmNode, crtc: crtc::Handle) {
|
||||||
&mut self,
|
|
||||||
niri: &mut Niri,
|
|
||||||
node: DrmNode,
|
|
||||||
connector: connector::Info,
|
|
||||||
crtc: crtc::Handle,
|
|
||||||
) {
|
|
||||||
debug!("disconnecting connector: {connector:?}");
|
|
||||||
|
|
||||||
let Some(device) = self.devices.get_mut(&node) else {
|
let Some(device) = self.devices.get_mut(&node) else {
|
||||||
|
debug!("disconnecting connector for crtc: {crtc:?}");
|
||||||
error!("missing device");
|
error!("missing device");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(surface) = device.surfaces.remove(&crtc) else {
|
let Some(surface) = device.surfaces.remove(&crtc) else {
|
||||||
|
debug!("disconnecting connector for crtc: {crtc:?}");
|
||||||
debug!("crtc wasn't enabled");
|
debug!("crtc wasn't enabled");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug!("disconnecting connector: {:?}", surface.name);
|
||||||
|
|
||||||
let output = niri
|
let output = niri
|
||||||
.global_space
|
.global_space
|
||||||
.outputs()
|
.outputs()
|
||||||
@@ -847,7 +793,7 @@ impl Tty {
|
|||||||
error!("missing output for crtc {crtc:?}");
|
error!("missing output for crtc {crtc:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
self.connectors.lock().unwrap().remove(&surface.name);
|
self.enabled_outputs.lock().unwrap().remove(&surface.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_vblank(
|
fn on_vblank(
|
||||||
@@ -1210,8 +1156,66 @@ impl Tty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
fn refresh_ipc_outputs(&self) {
|
||||||
self.connectors.clone()
|
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
|
||||||
|
|
||||||
|
let mut ipc_outputs = HashMap::new();
|
||||||
|
|
||||||
|
for device in self.devices.values() {
|
||||||
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||||
|
let connector_name = format!(
|
||||||
|
"{}-{}",
|
||||||
|
connector.interface().as_str(),
|
||||||
|
connector.interface_id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let physical_size = connector.size();
|
||||||
|
|
||||||
|
let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle())
|
||||||
|
.map(|info| (info.manufacturer, info.model))
|
||||||
|
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
|
||||||
|
|
||||||
|
let modes = connector
|
||||||
|
.modes()
|
||||||
|
.iter()
|
||||||
|
.map(|m| niri_ipc::Mode {
|
||||||
|
width: m.size().0,
|
||||||
|
height: m.size().1,
|
||||||
|
refresh_rate: Mode::from(*m).refresh as u32,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut output = niri_ipc::Output {
|
||||||
|
name: connector_name.clone(),
|
||||||
|
make,
|
||||||
|
model,
|
||||||
|
physical_size,
|
||||||
|
modes,
|
||||||
|
current_mode: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(surface) = device.surfaces.get(&crtc) {
|
||||||
|
let current = surface.compositor.pending_mode();
|
||||||
|
if let Some(current) = connector.modes().iter().position(|m| *m == current) {
|
||||||
|
output.current_mode = Some(current);
|
||||||
|
} else {
|
||||||
|
error!("connector mode list missing current mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc_outputs.insert(connector_name, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ipc_outputs.replace(ipc_outputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
@@ -1226,6 +1230,141 @@ impl Tty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
|
||||||
|
let _span = tracy_client::span!("Tty::on_output_config_changed");
|
||||||
|
|
||||||
|
let mut to_disconnect = vec![];
|
||||||
|
let mut to_connect = vec![];
|
||||||
|
|
||||||
|
for (&node, device) in &mut self.devices {
|
||||||
|
for surface in device.surfaces.values_mut() {
|
||||||
|
let crtc = surface.compositor.crtc();
|
||||||
|
|
||||||
|
let config = self
|
||||||
|
.config
|
||||||
|
.borrow()
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.name == surface.name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
if config.off {
|
||||||
|
to_disconnect.push((node, crtc));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need to change the mode.
|
||||||
|
let connector = surface
|
||||||
|
.compositor
|
||||||
|
.current_connectors()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
|
||||||
|
error!("missing enabled connector in drm_scanner");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((mode, fallback)) = pick_mode(connector, config.mode) else {
|
||||||
|
error!("couldn't pick mode for enabled connector");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if surface.compositor.current_mode() == mode {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = niri
|
||||||
|
.global_space
|
||||||
|
.outputs()
|
||||||
|
.find(|output| {
|
||||||
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
||||||
|
tty_state.node == node && tty_state.crtc == crtc
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
let Some(output) = output else {
|
||||||
|
error!("missing output for crtc: {crtc:?}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(output_state) = niri.output_state.get_mut(&output) else {
|
||||||
|
error!("missing state for output {:?}", surface.name);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if fallback {
|
||||||
|
let target = config.mode.unwrap();
|
||||||
|
warn!(
|
||||||
|
"output {:?}: configured mode {}x{}{} could not be found, \
|
||||||
|
falling back to preferred",
|
||||||
|
surface.name,
|
||||||
|
target.width,
|
||||||
|
target.height,
|
||||||
|
if let Some(refresh) = target.refresh {
|
||||||
|
format!("@{refresh}")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("output {:?}: picking mode: {mode:?}", surface.name);
|
||||||
|
if let Err(err) = surface.compositor.use_mode(mode) {
|
||||||
|
warn!("error changing mode: {err:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wl_mode = Mode::from(mode);
|
||||||
|
output.change_current_state(Some(wl_mode), None, None, None);
|
||||||
|
output.set_preferred(wl_mode);
|
||||||
|
output_state.frame_clock = FrameClock::new(Some(refresh_interval(mode)));
|
||||||
|
niri.output_resized(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||||
|
// Check if connected.
|
||||||
|
if connector.state() != connector::State::Connected {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already enabled.
|
||||||
|
if device.surfaces.contains_key(&crtc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_name = format!(
|
||||||
|
"{}-{}",
|
||||||
|
connector.interface().as_str(),
|
||||||
|
connector.interface_id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = self
|
||||||
|
.config
|
||||||
|
.borrow()
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.name == output_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !config.off {
|
||||||
|
to_connect.push((node, connector.clone(), crtc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (node, crtc) in to_disconnect {
|
||||||
|
self.connector_disconnected(niri, node, crtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (node, connector, crtc) in to_connect {
|
||||||
|
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
|
||||||
|
warn!("error connecting connector: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refresh_ipc_outputs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
|
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
|
||||||
@@ -1400,3 +1539,64 @@ fn queue_estimated_vblank_timer(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token);
|
output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pick_mode(
|
||||||
|
connector: &connector::Info,
|
||||||
|
target: Option<niri_config::Mode>,
|
||||||
|
) -> Option<(control::Mode, bool)> {
|
||||||
|
let mut mode = None;
|
||||||
|
let mut fallback = false;
|
||||||
|
|
||||||
|
if let Some(target) = target {
|
||||||
|
let refresh = target.refresh.map(|r| (r * 1000.).round() as i32);
|
||||||
|
|
||||||
|
for m in connector.modes() {
|
||||||
|
if m.size() != (target.width, target.height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(refresh) = refresh {
|
||||||
|
// If refresh is set, only pick modes with matching refresh.
|
||||||
|
let wl_mode = Mode::from(*m);
|
||||||
|
if wl_mode.refresh == refresh {
|
||||||
|
mode = Some(m);
|
||||||
|
}
|
||||||
|
} else if let Some(curr) = mode {
|
||||||
|
// If refresh isn't set, pick the mode with the highest refresh.
|
||||||
|
if curr.vrefresh() < m.vrefresh() {
|
||||||
|
mode = Some(m);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mode = Some(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.is_none() {
|
||||||
|
fallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.is_none() {
|
||||||
|
// Pick a preferred mode.
|
||||||
|
for m in connector.modes() {
|
||||||
|
if !m.mode_type().contains(ModeTypeFlags::PREFERRED) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(curr) = mode {
|
||||||
|
if curr.vrefresh() < m.vrefresh() {
|
||||||
|
mode = Some(m);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mode = Some(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.is_none() {
|
||||||
|
// Last attempt.
|
||||||
|
mode = connector.modes().first();
|
||||||
|
}
|
||||||
|
|
||||||
|
mode.map(|m| (*m, fallback))
|
||||||
|
}
|
||||||
|
|||||||
+37
-22
@@ -5,12 +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::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, ImportDma, ImportEgl, 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;
|
||||||
@@ -18,7 +19,6 @@ 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::{RedrawState, State};
|
||||||
use crate::utils::get_monotonic_time;
|
use crate::utils::get_monotonic_time;
|
||||||
use crate::Niri;
|
use crate::Niri;
|
||||||
@@ -28,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 {
|
||||||
@@ -39,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 {
|
||||||
@@ -61,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(),
|
||||||
)])));
|
)])));
|
||||||
@@ -90,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),
|
||||||
@@ -109,7 +119,8 @@ impl Winit {
|
|||||||
output,
|
output,
|
||||||
backend,
|
backend,
|
||||||
damage_tracker,
|
damage_tracker,
|
||||||
connectors,
|
ipc_outputs,
|
||||||
|
enabled_outputs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +223,11 @@ impl Winit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
|
||||||
self.connectors.clone()
|
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_memory(
|
||||||
|
&data,
|
||||||
|
Fourcc::Argb8888,
|
||||||
|
(width, height),
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
+12
-5
@@ -224,7 +224,7 @@ pub enum RenderCursor {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<TextureBuffer<GlesTexture>>>;
|
type TextureCache = HashMap<(CursorIcon, i32), Vec<Option<TextureBuffer<GlesTexture>>>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CursorTextureCache {
|
pub struct CursorTextureCache {
|
||||||
@@ -243,7 +243,7 @@ impl CursorTextureCache {
|
|||||||
scale: i32,
|
scale: i32,
|
||||||
cursor: &XCursor,
|
cursor: &XCursor,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> TextureBuffer<GlesTexture> {
|
) -> Option<TextureBuffer<GlesTexture>> {
|
||||||
self.cache
|
self.cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.entry((icon, scale))
|
.entry((icon, scale))
|
||||||
@@ -254,7 +254,7 @@ impl CursorTextureCache {
|
|||||||
.map(|frame| {
|
.map(|frame| {
|
||||||
let _span = tracy_client::span!("create TextureBuffer");
|
let _span = tracy_client::span!("create TextureBuffer");
|
||||||
|
|
||||||
TextureBuffer::from_memory(
|
let buffer = TextureBuffer::from_memory(
|
||||||
renderer,
|
renderer,
|
||||||
&frame.pixels_rgba,
|
&frame.pixels_rgba,
|
||||||
Fourcc::Abgr8888,
|
Fourcc::Abgr8888,
|
||||||
@@ -263,8 +263,15 @@ impl CursorTextureCache {
|
|||||||
scale,
|
scale,
|
||||||
Transform::Normal,
|
Transform::Normal,
|
||||||
None,
|
None,
|
||||||
)
|
);
|
||||||
.unwrap()
|
|
||||||
|
match buffer {
|
||||||
|
Ok(x) => Some(x),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error creating a cursor texture: {err:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.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};
|
||||||
|
|
||||||
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.
|
||||||
|
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||||
|
|
||||||
|
// 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![],
|
modes: vec![],
|
||||||
properties: HashMap::new(),
|
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 {
|
||||||
@@ -85,8 +116,8 @@ impl DisplayConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,18 @@ use super::Start;
|
|||||||
|
|
||||||
#[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>)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +64,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 +85,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 +156,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()));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,11 +199,11 @@ impl Stream {
|
|||||||
|
|
||||||
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 +228,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_memory(
|
||||||
|
&data,
|
||||||
|
Fourcc::Argb8888,
|
||||||
|
(width, height),
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
+29
-3
@@ -22,6 +22,9 @@ 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::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,
|
||||||
@@ -38,11 +41,11 @@ 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_constraints,
|
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
|
||||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||||
delegate_relative_pointer, delegate_seat, delegate_session_lock, delegate_tablet_manager,
|
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||||
delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::niri::State;
|
use crate::niri::{ClientState, State};
|
||||||
use crate::utils::output_size;
|
use crate::utils::output_size;
|
||||||
|
|
||||||
impl SeatHandler for State {
|
impl SeatHandler for State {
|
||||||
@@ -251,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);
|
||||||
|
|||||||
+110
-23
@@ -1,7 +1,9 @@
|
|||||||
use smithay::desktop::{
|
use smithay::desktop::{
|
||||||
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
|
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
|
||||||
PopupKind, PopupManager, Window, WindowSurfaceType,
|
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_positioner::ConstraintAdjustment;
|
||||||
@@ -12,6 +14,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
|||||||
use smithay::utils::{Logical, Rectangle, 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::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,
|
||||||
@@ -19,7 +22,7 @@ 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;
|
use crate::utils::clone2;
|
||||||
|
|
||||||
impl XdgShellHandler for State {
|
impl XdgShellHandler for State {
|
||||||
@@ -90,8 +93,93 @@ impl XdgShellHandler for State {
|
|||||||
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
|
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) {
|
||||||
@@ -116,9 +204,6 @@ 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
|
||||||
@@ -192,40 +277,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_memory(
|
||||||
|
&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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+194
-14
@@ -1,6 +1,7 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use niri_config::{Action, Binds, LayoutAction, Modifiers};
|
||||||
use smithay::backend::input::{
|
use smithay::backend::input::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
|
||||||
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
|
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
|
||||||
@@ -20,7 +21,6 @@ use smithay::utils::{Logical, Point, SERIAL_COUNTER};
|
|||||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
|
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
|
||||||
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
|
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
|
||||||
|
|
||||||
use crate::config::{Action, Binds, LayoutAction, Modifiers};
|
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
use crate::screenshot_ui::ScreenshotUi;
|
use crate::screenshot_ui::ScreenshotUi;
|
||||||
use crate::utils::{center, get_monotonic_time, spawn};
|
use crate::utils::{center, get_monotonic_time, spawn};
|
||||||
@@ -54,6 +54,16 @@ impl State {
|
|||||||
self.niri.activate_monitors(&self.backend);
|
self.niri.activate_monitors(&self.backend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hide_hotkey_overlay =
|
||||||
|
self.niri.hotkey_overlay.is_open() && should_hide_hotkey_overlay(&event);
|
||||||
|
|
||||||
|
let hide_exit_confirm_dialog = self
|
||||||
|
.niri
|
||||||
|
.exit_confirm_dialog
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |d| d.is_open())
|
||||||
|
&& should_hide_exit_confirm_dialog(&event);
|
||||||
|
|
||||||
use InputEvent::*;
|
use InputEvent::*;
|
||||||
match event {
|
match event {
|
||||||
DeviceAdded { device } => self.on_device_added(device),
|
DeviceAdded { device } => self.on_device_added(device),
|
||||||
@@ -82,6 +92,18 @@ impl State {
|
|||||||
TouchFrame { .. } => (),
|
TouchFrame { .. } => (),
|
||||||
Special(_) => (),
|
Special(_) => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do this last so that screenshot still gets it.
|
||||||
|
// FIXME: do this in a less cursed fashion somehow.
|
||||||
|
if hide_hotkey_overlay && self.niri.hotkey_overlay.hide() {
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||||
|
if hide_exit_confirm_dialog && dialog.hide() {
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_libinput_event(&mut self, event: &mut InputEvent<LibinputInputBackend>) {
|
pub fn process_libinput_event(&mut self, event: &mut InputEvent<LibinputInputBackend>) {
|
||||||
@@ -89,14 +111,7 @@ impl State {
|
|||||||
|
|
||||||
match event {
|
match event {
|
||||||
InputEvent::DeviceAdded { device } => {
|
InputEvent::DeviceAdded { device } => {
|
||||||
// According to Mutter code, this setting is specific to touchpads.
|
self.niri.devices.insert(device.clone());
|
||||||
let is_touchpad = device.config_tap_finger_count() > 0;
|
|
||||||
if is_touchpad {
|
|
||||||
let c = &self.niri.config.borrow().input.touchpad;
|
|
||||||
let _ = device.config_tap_set_enabled(c.tap);
|
|
||||||
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
|
||||||
let _ = device.config_accel_set_speed(c.accel_speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if device.has_capability(input::DeviceCapability::TabletTool) {
|
if device.has_capability(input::DeviceCapability::TabletTool) {
|
||||||
match device.size() {
|
match device.size() {
|
||||||
@@ -110,9 +125,12 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply_libinput_settings(&self.niri.config.borrow().input, device);
|
||||||
}
|
}
|
||||||
InputEvent::DeviceRemoved { device } => {
|
InputEvent::DeviceRemoved { device } => {
|
||||||
self.niri.tablets.remove(device);
|
self.niri.tablets.remove(device);
|
||||||
|
self.niri.devices.remove(device);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
@@ -199,6 +217,13 @@ impl State {
|
|||||||
let key_code = event.key_code();
|
let key_code = event.key_code();
|
||||||
let modified = keysym.modified_sym();
|
let modified = keysym.modified_sym();
|
||||||
let raw = keysym.raw_latin_sym_or_raw_current_sym();
|
let raw = keysym.raw_latin_sym_or_raw_current_sym();
|
||||||
|
|
||||||
|
if let Some(dialog) = &this.niri.exit_confirm_dialog {
|
||||||
|
if dialog.is_open() && pressed && raw == Some(Keysym::Return) {
|
||||||
|
this.niri.stop_signal.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
should_intercept_key(
|
should_intercept_key(
|
||||||
&mut this.niri.suppressed_keys,
|
&mut this.niri.suppressed_keys,
|
||||||
bindings,
|
bindings,
|
||||||
@@ -227,9 +252,15 @@ impl State {
|
|||||||
|
|
||||||
match action {
|
match action {
|
||||||
Action::Quit => {
|
Action::Quit => {
|
||||||
|
if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
|
||||||
|
if dialog.show() {
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
info!("quitting because quit bind was pressed");
|
info!("quitting because quit bind was pressed");
|
||||||
self.niri.stop_signal.stop()
|
self.niri.stop_signal.stop()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Action::ChangeVt(vt) => {
|
Action::ChangeVt(vt) => {
|
||||||
self.backend.change_vt(vt);
|
self.backend.change_vt(vt);
|
||||||
// Changing `VT` may not deliver the key releases, so clear the state.
|
// Changing `VT` may not deliver the key releases, so clear the state.
|
||||||
@@ -311,6 +342,8 @@ impl State {
|
|||||||
let focus = self.niri.layout.focus().cloned();
|
let focus = self.niri.layout.focus().cloned();
|
||||||
if let Some(window) = focus {
|
if let Some(window) = focus {
|
||||||
self.niri.layout.toggle_fullscreen(&window);
|
self.niri.layout.toggle_fullscreen(&window);
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::SwitchLayout(action) => {
|
Action::SwitchLayout(action) => {
|
||||||
@@ -364,21 +397,33 @@ impl State {
|
|||||||
}
|
}
|
||||||
Action::FocusColumnLeft => {
|
Action::FocusColumnLeft => {
|
||||||
self.niri.layout.focus_left();
|
self.niri.layout.focus_left();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
Action::FocusColumnRight => {
|
Action::FocusColumnRight => {
|
||||||
self.niri.layout.focus_right();
|
self.niri.layout.focus_right();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
Action::FocusColumnFirst => {
|
Action::FocusColumnFirst => {
|
||||||
self.niri.layout.focus_column_first();
|
self.niri.layout.focus_column_first();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
Action::FocusColumnLast => {
|
Action::FocusColumnLast => {
|
||||||
self.niri.layout.focus_column_last();
|
self.niri.layout.focus_column_last();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
Action::FocusWindowDown => {
|
Action::FocusWindowDown => {
|
||||||
self.niri.layout.focus_down();
|
self.niri.layout.focus_down();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
Action::FocusWindowUp => {
|
Action::FocusWindowUp => {
|
||||||
self.niri.layout.focus_up();
|
self.niri.layout.focus_up();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
Action::FocusWindowOrWorkspaceDown => {
|
Action::FocusWindowOrWorkspaceDown => {
|
||||||
self.niri.layout.focus_window_or_workspace_down();
|
self.niri.layout.focus_window_or_workspace_down();
|
||||||
@@ -406,6 +451,22 @@ impl State {
|
|||||||
// FIXME: granular
|
// FIXME: granular
|
||||||
self.niri.queue_redraw_all();
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
|
Action::MoveColumnToWorkspaceDown => {
|
||||||
|
self.niri.layout.move_column_to_workspace_down();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
Action::MoveColumnToWorkspaceUp => {
|
||||||
|
self.niri.layout.move_column_to_workspace_up();
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
Action::MoveColumnToWorkspace(idx) => {
|
||||||
|
let idx = idx.saturating_sub(1) as usize;
|
||||||
|
self.niri.layout.move_column_to_workspace(idx);
|
||||||
|
// FIXME: granular
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
Action::FocusWorkspaceDown => {
|
Action::FocusWorkspaceDown => {
|
||||||
self.niri.layout.switch_workspace_down();
|
self.niri.layout.switch_workspace_down();
|
||||||
// FIXME: granular
|
// FIXME: granular
|
||||||
@@ -501,12 +562,41 @@ impl State {
|
|||||||
self.move_cursor_to_output(&output);
|
self.move_cursor_to_output(&output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Action::MoveColumnToMonitorLeft => {
|
||||||
|
if let Some(output) = self.niri.output_left() {
|
||||||
|
self.niri.layout.move_column_to_output(&output);
|
||||||
|
self.move_cursor_to_output(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::MoveColumnToMonitorRight => {
|
||||||
|
if let Some(output) = self.niri.output_right() {
|
||||||
|
self.niri.layout.move_column_to_output(&output);
|
||||||
|
self.move_cursor_to_output(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::MoveColumnToMonitorDown => {
|
||||||
|
if let Some(output) = self.niri.output_down() {
|
||||||
|
self.niri.layout.move_column_to_output(&output);
|
||||||
|
self.move_cursor_to_output(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::MoveColumnToMonitorUp => {
|
||||||
|
if let Some(output) = self.niri.output_up() {
|
||||||
|
self.niri.layout.move_column_to_output(&output);
|
||||||
|
self.move_cursor_to_output(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
Action::SetColumnWidth(change) => {
|
Action::SetColumnWidth(change) => {
|
||||||
self.niri.layout.set_column_width(change);
|
self.niri.layout.set_column_width(change);
|
||||||
}
|
}
|
||||||
Action::SetWindowHeight(change) => {
|
Action::SetWindowHeight(change) => {
|
||||||
self.niri.layout.set_window_height(change);
|
self.niri.layout.set_window_height(change);
|
||||||
}
|
}
|
||||||
|
Action::ShowHotkeyOverlay => {
|
||||||
|
if self.niri.hotkey_overlay.show() {
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,12 +844,18 @@ impl State {
|
|||||||
|
|
||||||
let button_state = event.state();
|
let button_state = event.state();
|
||||||
|
|
||||||
if ButtonState::Pressed == button_state && !pointer.is_grabbed() {
|
if ButtonState::Pressed == button_state {
|
||||||
if let Some(window) = self.niri.window_under_cursor() {
|
if let Some(window) = self.niri.window_under_cursor() {
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
self.niri.layout.activate_window(&window);
|
self.niri.layout.activate_window(&window);
|
||||||
|
|
||||||
|
// FIXME: granular.
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
} else if let Some(output) = self.niri.output_under_cursor() {
|
} else if let Some(output) = self.niri.output_under_cursor() {
|
||||||
self.niri.layout.activate_output(&output);
|
self.niri.layout.activate_output(&output);
|
||||||
|
|
||||||
|
// FIXME: granular.
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1280,9 +1376,7 @@ fn action(
|
|||||||
comp_mod = Modifiers::empty();
|
comp_mod = Modifiers::empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(raw) = raw else {
|
let raw = raw?;
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
for bind in &bindings.0 {
|
for bind in &bindings.0 {
|
||||||
if bind.key.keysym != raw {
|
if bind.key.keysym != raw {
|
||||||
@@ -1318,6 +1412,36 @@ fn should_activate_monitors<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_hide_hotkey_overlay<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||||
|
match event {
|
||||||
|
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
|
||||||
|
InputEvent::PointerButton { .. }
|
||||||
|
| InputEvent::PointerAxis { .. }
|
||||||
|
| InputEvent::GestureSwipeBegin { .. }
|
||||||
|
| InputEvent::GesturePinchBegin { .. }
|
||||||
|
| InputEvent::TouchDown { .. }
|
||||||
|
| InputEvent::TouchMotion { .. }
|
||||||
|
| InputEvent::TabletToolTip { .. }
|
||||||
|
| InputEvent::TabletToolButton { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_hide_exit_confirm_dialog<I: InputBackend>(event: &InputEvent<I>) -> bool {
|
||||||
|
match event {
|
||||||
|
InputEvent::Keyboard { event } if event.state() == KeyState::Pressed => true,
|
||||||
|
InputEvent::PointerButton { .. }
|
||||||
|
| InputEvent::PointerAxis { .. }
|
||||||
|
| InputEvent::GestureSwipeBegin { .. }
|
||||||
|
| InputEvent::GesturePinchBegin { .. }
|
||||||
|
| InputEvent::TouchDown { .. }
|
||||||
|
| InputEvent::TouchMotion { .. }
|
||||||
|
| InputEvent::TabletToolTip { .. }
|
||||||
|
| InputEvent::TabletToolButton { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn allowed_when_locked(action: &Action) -> bool {
|
fn allowed_when_locked(action: &Action) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
action,
|
action,
|
||||||
@@ -1336,10 +1460,66 @@ fn allowed_during_screenshot(action: &Action) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::Device) {
|
||||||
|
// According to Mutter code, this setting is specific to touchpads.
|
||||||
|
let is_touchpad = device.config_tap_finger_count() > 0;
|
||||||
|
if is_touchpad {
|
||||||
|
let c = &config.touchpad;
|
||||||
|
let _ = device.config_tap_set_enabled(c.tap);
|
||||||
|
let _ = device.config_dwt_set_enabled(c.dwt);
|
||||||
|
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||||
|
let _ = device.config_accel_set_speed(c.accel_speed);
|
||||||
|
|
||||||
|
if let Some(accel_profile) = c.accel_profile {
|
||||||
|
let _ = device.config_accel_set_profile(accel_profile.into());
|
||||||
|
} else if let Some(default) = device.config_accel_default_profile() {
|
||||||
|
let _ = device.config_accel_set_profile(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tap_button_map) = c.tap_button_map {
|
||||||
|
let _ = device.config_tap_set_button_map(tap_button_map.into());
|
||||||
|
} else if let Some(default) = device.config_tap_default_button_map() {
|
||||||
|
let _ = device.config_tap_set_button_map(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is how Mutter tells apart mice.
|
||||||
|
let mut is_trackball = false;
|
||||||
|
let mut is_trackpoint = false;
|
||||||
|
if let Some(udev_device) = unsafe { device.udev_device() } {
|
||||||
|
if udev_device.property_value("ID_INPUT_TRACKBALL").is_some() {
|
||||||
|
is_trackball = true;
|
||||||
|
}
|
||||||
|
if udev_device
|
||||||
|
.property_value("ID_INPUT_POINTINGSTICK")
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
is_trackpoint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_mouse = device.has_capability(input::DeviceCapability::Pointer)
|
||||||
|
&& !is_touchpad
|
||||||
|
&& !is_trackball
|
||||||
|
&& !is_trackpoint;
|
||||||
|
if is_mouse {
|
||||||
|
let c = &config.mouse;
|
||||||
|
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||||
|
let _ = device.config_accel_set_speed(c.accel_speed);
|
||||||
|
|
||||||
|
if let Some(accel_profile) = c.accel_profile {
|
||||||
|
let _ = device.config_accel_set_profile(accel_profile.into());
|
||||||
|
} else if let Some(default) = device.config_accel_default_profile() {
|
||||||
|
let _ = device.config_accel_set_profile(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use niri_config::{Action, Bind, Binds, Key, Modifiers};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{Action, Bind, Binds, Key, Modifiers};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bindings_suppress_keys() {
|
fn bindings_suppress_keys() {
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
use std::iter::zip;
|
use std::iter::zip;
|
||||||
|
|
||||||
use arrayvec::ArrayVec;
|
use arrayvec::ArrayVec;
|
||||||
|
use niri_config::{self, Color};
|
||||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use smithay::backend::renderer::element::Kind;
|
use smithay::backend::renderer::element::Kind;
|
||||||
use smithay::utils::{Logical, Point, Scale, Size};
|
use smithay::utils::{Logical, Point, Scale, Size};
|
||||||
|
|
||||||
use crate::config::{self, Color};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FocusRing {
|
pub struct FocusRing {
|
||||||
buffers: [SolidColorBuffer; 4],
|
buffers: [SolidColorBuffer; 4],
|
||||||
@@ -21,7 +20,7 @@ pub struct FocusRing {
|
|||||||
pub type FocusRingRenderElement = SolidColorRenderElement;
|
pub type FocusRingRenderElement = SolidColorRenderElement;
|
||||||
|
|
||||||
impl FocusRing {
|
impl FocusRing {
|
||||||
pub fn new(config: config::FocusRing) -> Self {
|
pub fn new(config: niri_config::FocusRing) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffers: Default::default(),
|
buffers: Default::default(),
|
||||||
locations: Default::default(),
|
locations: Default::default(),
|
||||||
@@ -33,7 +32,7 @@ impl FocusRing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config(&mut self, config: config::FocusRing) {
|
pub fn update_config(&mut self, config: niri_config::FocusRing) {
|
||||||
self.is_off = config.off;
|
self.is_off = config.off;
|
||||||
self.width = config.width.into();
|
self.width = config.width.into();
|
||||||
self.active_color = config.active_color;
|
self.active_color = config.active_color;
|
||||||
|
|||||||
+276
-20
@@ -33,6 +33,7 @@ use std::mem;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri_config::{self, CenterFocusedColumn, Config, SizeChange, Struts};
|
||||||
use smithay::backend::renderer::element::AsRenderElements;
|
use smithay::backend::renderer::element::AsRenderElements;
|
||||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||||
use smithay::desktop::space::SpaceElement;
|
use smithay::desktop::space::SpaceElement;
|
||||||
@@ -48,10 +49,9 @@ use smithay::wayland::shell::xdg::SurfaceCachedState;
|
|||||||
pub use self::monitor::MonitorRenderElement;
|
pub use self::monitor::MonitorRenderElement;
|
||||||
use self::monitor::{Monitor, WorkspaceSwitch, WorkspaceSwitchGesture};
|
use self::monitor::{Monitor, WorkspaceSwitch, WorkspaceSwitchGesture};
|
||||||
use self::workspace::{
|
use self::workspace::{
|
||||||
compute_working_area, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
|
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
|
||||||
};
|
};
|
||||||
use crate::animation::Animation;
|
use crate::animation::Animation;
|
||||||
use crate::config::{self, Config, SizeChange, Struts};
|
|
||||||
use crate::utils::output_size;
|
use crate::utils::output_size;
|
||||||
|
|
||||||
mod focus_ring;
|
mod focus_ring;
|
||||||
@@ -137,8 +137,9 @@ pub struct Options {
|
|||||||
gaps: i32,
|
gaps: i32,
|
||||||
/// Extra padding around the working area in logical pixels.
|
/// Extra padding around the working area in logical pixels.
|
||||||
struts: Struts,
|
struts: Struts,
|
||||||
focus_ring: config::FocusRing,
|
focus_ring: niri_config::FocusRing,
|
||||||
border: config::FocusRing,
|
border: niri_config::FocusRing,
|
||||||
|
center_focused_column: CenterFocusedColumn,
|
||||||
/// Column widths that `toggle_width()` switches between.
|
/// Column widths that `toggle_width()` switches between.
|
||||||
preset_widths: Vec<ColumnWidth>,
|
preset_widths: Vec<ColumnWidth>,
|
||||||
/// Initial width for new columns.
|
/// Initial width for new columns.
|
||||||
@@ -151,7 +152,8 @@ impl Default for Options {
|
|||||||
gaps: 16,
|
gaps: 16,
|
||||||
struts: Default::default(),
|
struts: Default::default(),
|
||||||
focus_ring: Default::default(),
|
focus_ring: Default::default(),
|
||||||
border: config::default_border(),
|
border: niri_config::default_border(),
|
||||||
|
center_focused_column: Default::default(),
|
||||||
preset_widths: vec![
|
preset_widths: vec![
|
||||||
ColumnWidth::Proportion(1. / 3.),
|
ColumnWidth::Proportion(1. / 3.),
|
||||||
ColumnWidth::Proportion(0.5),
|
ColumnWidth::Proportion(0.5),
|
||||||
@@ -164,7 +166,8 @@ impl Default for Options {
|
|||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
fn from_config(config: &Config) -> Self {
|
fn from_config(config: &Config) -> Self {
|
||||||
let preset_column_widths = &config.preset_column_widths;
|
let layout = &config.layout;
|
||||||
|
let preset_column_widths = &layout.preset_column_widths;
|
||||||
|
|
||||||
let preset_widths = if preset_column_widths.is_empty() {
|
let preset_widths = if preset_column_widths.is_empty() {
|
||||||
Options::default().preset_widths
|
Options::default().preset_widths
|
||||||
@@ -178,17 +181,18 @@ impl Options {
|
|||||||
|
|
||||||
// Missing default_column_width maps to Some(ColumnWidth::Proportion(0.5)),
|
// Missing default_column_width maps to Some(ColumnWidth::Proportion(0.5)),
|
||||||
// while present, but empty, maps to None.
|
// while present, but empty, maps to None.
|
||||||
let default_width = config
|
let default_width = layout
|
||||||
.default_column_width
|
.default_column_width
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|w| w.0.first().copied().map(ColumnWidth::from))
|
.map(|w| w.0.first().copied().map(ColumnWidth::from))
|
||||||
.unwrap_or(Some(ColumnWidth::Proportion(0.5)));
|
.unwrap_or(Some(ColumnWidth::Proportion(0.5)));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
gaps: config.gaps.into(),
|
gaps: layout.gaps.into(),
|
||||||
struts: config.struts,
|
struts: layout.struts,
|
||||||
focus_ring: config.focus_ring,
|
focus_ring: layout.focus_ring,
|
||||||
border: config.border,
|
border: layout.border,
|
||||||
|
center_focused_column: layout.center_focused_column,
|
||||||
preset_widths,
|
preset_widths,
|
||||||
default_width,
|
default_width,
|
||||||
}
|
}
|
||||||
@@ -306,11 +310,21 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
} => {
|
} => {
|
||||||
let primary = &mut monitors[primary_idx];
|
let primary = &mut monitors[primary_idx];
|
||||||
|
|
||||||
|
let mut stopped_primary_ws_switch = false;
|
||||||
|
|
||||||
let mut workspaces = vec![];
|
let mut workspaces = vec![];
|
||||||
for i in (0..primary.workspaces.len()).rev() {
|
for i in (0..primary.workspaces.len()).rev() {
|
||||||
if primary.workspaces[i].original_output == id {
|
if primary.workspaces[i].original_output == id {
|
||||||
let ws = primary.workspaces.remove(i);
|
let ws = primary.workspaces.remove(i);
|
||||||
|
|
||||||
|
// FIXME: this can be coded in a way that the workspace switch won't be
|
||||||
|
// affected if the removed workspace is invisible. But this is good enough
|
||||||
|
// for now.
|
||||||
|
if primary.workspace_switch.is_some() {
|
||||||
|
primary.workspace_switch = None;
|
||||||
|
stopped_primary_ws_switch = true;
|
||||||
|
}
|
||||||
|
|
||||||
// The user could've closed a window while remaining on this workspace, on
|
// The user could've closed a window while remaining on this workspace, on
|
||||||
// another monitor. However, we will add an empty workspace in the end
|
// another monitor. However, we will add an empty workspace in the end
|
||||||
// instead.
|
// instead.
|
||||||
@@ -324,6 +338,12 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we stopped a workspace switch, then we might need to clean up workspaces.
|
||||||
|
if stopped_primary_ws_switch {
|
||||||
|
primary.clean_up_workspaces();
|
||||||
|
}
|
||||||
|
|
||||||
workspaces.reverse();
|
workspaces.reverse();
|
||||||
|
|
||||||
// Make sure there's always an empty workspace.
|
// Make sure there's always an empty workspace.
|
||||||
@@ -453,6 +473,29 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_column_by_idx(
|
||||||
|
&mut self,
|
||||||
|
monitor_idx: usize,
|
||||||
|
workspace_idx: usize,
|
||||||
|
column: Column<W>,
|
||||||
|
activate: bool,
|
||||||
|
) {
|
||||||
|
let MonitorSet::Normal {
|
||||||
|
monitors,
|
||||||
|
active_monitor_idx,
|
||||||
|
..
|
||||||
|
} = &mut self.monitor_set
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
monitors[monitor_idx].add_column(workspace_idx, column, activate);
|
||||||
|
|
||||||
|
if activate {
|
||||||
|
*active_monitor_idx = monitor_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds a new window to the layout.
|
/// Adds a new window to the layout.
|
||||||
///
|
///
|
||||||
/// Returns an output that the window was added to, if there were any outputs.
|
/// Returns an output that the window was added to, if there were any outputs.
|
||||||
@@ -515,6 +558,7 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
if !ws.has_windows()
|
if !ws.has_windows()
|
||||||
&& idx != mon.active_workspace_idx
|
&& idx != mon.active_workspace_idx
|
||||||
&& idx != mon.workspaces.len() - 1
|
&& idx != mon.workspaces.len() - 1
|
||||||
|
&& mon.workspace_switch.is_none()
|
||||||
{
|
{
|
||||||
mon.workspaces.remove(idx);
|
mon.workspaces.remove(idx);
|
||||||
|
|
||||||
@@ -610,6 +654,8 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_output_size(&mut self, output: &Output) {
|
pub fn update_output_size(&mut self, output: &Output) {
|
||||||
|
let _span = tracy_client::span!("Layout::update_output_size");
|
||||||
|
|
||||||
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
|
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
|
||||||
panic!()
|
panic!()
|
||||||
};
|
};
|
||||||
@@ -621,6 +667,7 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
|
|
||||||
for ws in &mut mon.workspaces {
|
for ws in &mut mon.workspaces {
|
||||||
ws.set_view_size(view_size, working_area);
|
ws.set_view_size(view_size, working_area);
|
||||||
|
ws.update_output_scale_transform();
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -893,6 +940,27 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
monitor.move_to_workspace(idx);
|
monitor.move_to_workspace(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_workspace_up(&mut self) {
|
||||||
|
let Some(monitor) = self.active_monitor() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
monitor.move_column_to_workspace_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_workspace_down(&mut self) {
|
||||||
|
let Some(monitor) = self.active_monitor() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
monitor.move_column_to_workspace_down();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_workspace(&mut self, idx: usize) {
|
||||||
|
let Some(monitor) = self.active_monitor() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
monitor.move_column_to_workspace(idx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn switch_workspace_up(&mut self) {
|
pub fn switch_workspace_up(&mut self) {
|
||||||
let Some(monitor) = self.active_monitor() else {
|
let Some(monitor) = self.active_monitor() else {
|
||||||
return;
|
return;
|
||||||
@@ -1010,6 +1078,14 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
"monitor options must be synchronized with layout"
|
"monitor options must be synchronized with layout"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(WorkspaceSwitch::Animation(anim)) = &monitor.workspace_switch {
|
||||||
|
let before_idx = anim.from() as usize;
|
||||||
|
let after_idx = anim.to() as usize;
|
||||||
|
|
||||||
|
assert!(before_idx < monitor.workspaces.len());
|
||||||
|
assert!(after_idx < monitor.workspaces.len());
|
||||||
|
}
|
||||||
|
|
||||||
let monitor_id = OutputId::new(&monitor.output);
|
let monitor_id = OutputId::new(&monitor.output);
|
||||||
|
|
||||||
if idx == primary_idx {
|
if idx == primary_idx {
|
||||||
@@ -1180,6 +1256,30 @@ impl<W: LayoutElement> Layout<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn move_column_to_output(&mut self, output: &Output) {
|
||||||
|
if let MonitorSet::Normal {
|
||||||
|
monitors,
|
||||||
|
active_monitor_idx,
|
||||||
|
..
|
||||||
|
} = &mut self.monitor_set
|
||||||
|
{
|
||||||
|
let new_idx = monitors
|
||||||
|
.iter()
|
||||||
|
.position(|mon| &mon.output == output)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let current = &mut monitors[*active_monitor_idx];
|
||||||
|
let ws = current.active_workspace();
|
||||||
|
if !ws.has_windows() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let column = ws.remove_column_by_idx(ws.active_column_idx);
|
||||||
|
|
||||||
|
let workspace_idx = monitors[new_idx].active_workspace_idx;
|
||||||
|
self.add_column_by_idx(new_idx, workspace_idx, column, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_window_to_output(&mut self, window: W, output: &Output) {
|
pub fn move_window_to_output(&mut self, window: W, output: &Output) {
|
||||||
if !matches!(&self.monitor_set, MonitorSet::Normal { .. }) {
|
if !matches!(&self.monitor_set, MonitorSet::Normal { .. }) {
|
||||||
return;
|
return;
|
||||||
@@ -1604,9 +1704,13 @@ mod tests {
|
|||||||
MoveWindowToWorkspaceDown,
|
MoveWindowToWorkspaceDown,
|
||||||
MoveWindowToWorkspaceUp,
|
MoveWindowToWorkspaceUp,
|
||||||
MoveWindowToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
|
MoveWindowToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
|
||||||
|
MoveColumnToWorkspaceDown,
|
||||||
|
MoveColumnToWorkspaceUp,
|
||||||
|
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
|
||||||
MoveWorkspaceDown,
|
MoveWorkspaceDown,
|
||||||
MoveWorkspaceUp,
|
MoveWorkspaceUp,
|
||||||
MoveWindowToOutput(#[proptest(strategy = "1..=5u8")] u8),
|
MoveWindowToOutput(#[proptest(strategy = "1..=5u8")] u8),
|
||||||
|
MoveColumnToOutput(#[proptest(strategy = "1..=5u8")] u8),
|
||||||
SwitchPresetColumnWidth,
|
SwitchPresetColumnWidth,
|
||||||
MaximizeColumn,
|
MaximizeColumn,
|
||||||
SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
|
SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
|
||||||
@@ -1725,6 +1829,9 @@ mod tests {
|
|||||||
Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(),
|
Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(),
|
||||||
Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(),
|
Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(),
|
||||||
Op::MoveWindowToWorkspace(idx) => layout.move_to_workspace(idx),
|
Op::MoveWindowToWorkspace(idx) => layout.move_to_workspace(idx),
|
||||||
|
Op::MoveColumnToWorkspaceDown => layout.move_column_to_workspace_down(),
|
||||||
|
Op::MoveColumnToWorkspaceUp => layout.move_column_to_workspace_up(),
|
||||||
|
Op::MoveColumnToWorkspace(idx) => layout.move_column_to_workspace(idx),
|
||||||
Op::MoveWindowToOutput(id) => {
|
Op::MoveWindowToOutput(id) => {
|
||||||
let name = format!("output{id}");
|
let name = format!("output{id}");
|
||||||
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
|
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
|
||||||
@@ -1733,6 +1840,14 @@ mod tests {
|
|||||||
|
|
||||||
layout.move_to_output(&output);
|
layout.move_to_output(&output);
|
||||||
}
|
}
|
||||||
|
Op::MoveColumnToOutput(id) => {
|
||||||
|
let name = format!("output{id}");
|
||||||
|
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout.move_column_to_output(&output);
|
||||||
|
}
|
||||||
Op::MoveWorkspaceDown => layout.move_workspace_down(),
|
Op::MoveWorkspaceDown => layout.move_workspace_down(),
|
||||||
Op::MoveWorkspaceUp => layout.move_workspace_up(),
|
Op::MoveWorkspaceUp => layout.move_workspace_up(),
|
||||||
Op::SwitchPresetColumnWidth => layout.toggle_width(),
|
Op::SwitchPresetColumnWidth => layout.toggle_width(),
|
||||||
@@ -1851,6 +1966,11 @@ mod tests {
|
|||||||
Op::MoveWindowToWorkspace(1),
|
Op::MoveWindowToWorkspace(1),
|
||||||
Op::MoveWindowToWorkspace(2),
|
Op::MoveWindowToWorkspace(2),
|
||||||
Op::MoveWindowToWorkspace(3),
|
Op::MoveWindowToWorkspace(3),
|
||||||
|
Op::MoveColumnToWorkspaceDown,
|
||||||
|
Op::MoveColumnToWorkspaceUp,
|
||||||
|
Op::MoveColumnToWorkspace(1),
|
||||||
|
Op::MoveColumnToWorkspace(2),
|
||||||
|
Op::MoveColumnToWorkspace(3),
|
||||||
Op::MoveWindowDown,
|
Op::MoveWindowDown,
|
||||||
Op::MoveWindowDownOrToWorkspaceDown,
|
Op::MoveWindowDownOrToWorkspaceDown,
|
||||||
Op::MoveWindowUp,
|
Op::MoveWindowUp,
|
||||||
@@ -1973,6 +2093,11 @@ mod tests {
|
|||||||
Op::MoveWindowToWorkspace(1),
|
Op::MoveWindowToWorkspace(1),
|
||||||
Op::MoveWindowToWorkspace(2),
|
Op::MoveWindowToWorkspace(2),
|
||||||
Op::MoveWindowToWorkspace(3),
|
Op::MoveWindowToWorkspace(3),
|
||||||
|
Op::MoveColumnToWorkspaceDown,
|
||||||
|
Op::MoveColumnToWorkspaceUp,
|
||||||
|
Op::MoveColumnToWorkspace(1),
|
||||||
|
Op::MoveColumnToWorkspace(2),
|
||||||
|
Op::MoveColumnToWorkspace(3),
|
||||||
Op::MoveWindowDown,
|
Op::MoveWindowDown,
|
||||||
Op::MoveWindowDownOrToWorkspaceDown,
|
Op::MoveWindowDownOrToWorkspaceDown,
|
||||||
Op::MoveWindowUp,
|
Op::MoveWindowUp,
|
||||||
@@ -2186,8 +2311,145 @@ mod tests {
|
|||||||
check_ops_with_options(options, &ops);
|
check_ops_with_options(options, &ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arbitrary_border() -> impl Strategy<Value = u16> {
|
#[test]
|
||||||
prop_oneof![Just(0), (1..=u16::MAX)]
|
fn workspace_cleanup_during_switch() {
|
||||||
|
let ops = [
|
||||||
|
Op::AddOutput(1),
|
||||||
|
Op::AddWindow {
|
||||||
|
id: 1,
|
||||||
|
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||||
|
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||||
|
},
|
||||||
|
Op::FocusWorkspaceDown,
|
||||||
|
Op::CloseWindow(1),
|
||||||
|
];
|
||||||
|
|
||||||
|
check_ops(&ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_transfer_during_switch() {
|
||||||
|
let ops = [
|
||||||
|
Op::AddOutput(1),
|
||||||
|
Op::AddWindow {
|
||||||
|
id: 1,
|
||||||
|
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||||
|
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||||
|
},
|
||||||
|
Op::AddOutput(2),
|
||||||
|
Op::FocusOutput(2),
|
||||||
|
Op::AddWindow {
|
||||||
|
id: 2,
|
||||||
|
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||||
|
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||||
|
},
|
||||||
|
Op::RemoveOutput(1),
|
||||||
|
Op::FocusWorkspaceDown,
|
||||||
|
Op::FocusWorkspaceDown,
|
||||||
|
Op::AddOutput(1),
|
||||||
|
];
|
||||||
|
|
||||||
|
check_ops(&ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_transfer_during_switch_from_last() {
|
||||||
|
let ops = [
|
||||||
|
Op::AddOutput(1),
|
||||||
|
Op::AddWindow {
|
||||||
|
id: 1,
|
||||||
|
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||||
|
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||||
|
},
|
||||||
|
Op::AddOutput(2),
|
||||||
|
Op::RemoveOutput(1),
|
||||||
|
Op::FocusWorkspaceUp,
|
||||||
|
Op::AddOutput(1),
|
||||||
|
];
|
||||||
|
|
||||||
|
check_ops(&ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_transfer_during_switch_gets_cleaned_up() {
|
||||||
|
let ops = [
|
||||||
|
Op::AddOutput(1),
|
||||||
|
Op::AddWindow {
|
||||||
|
id: 1,
|
||||||
|
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||||
|
min_max_size: (Size::from((0, 0)), Size::from((i32::MAX, i32::MAX))),
|
||||||
|
},
|
||||||
|
Op::RemoveOutput(1),
|
||||||
|
Op::AddOutput(2),
|
||||||
|
Op::MoveColumnToWorkspaceDown,
|
||||||
|
Op::MoveColumnToWorkspaceDown,
|
||||||
|
Op::AddOutput(1),
|
||||||
|
];
|
||||||
|
|
||||||
|
check_ops(&ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arbitrary_spacing() -> impl Strategy<Value = u16> {
|
||||||
|
// Give equal weight to:
|
||||||
|
// - 0: the element is disabled
|
||||||
|
// - 4: some reasonable value
|
||||||
|
// - random value, likely unreasonably big
|
||||||
|
prop_oneof![Just(0), Just(4), (1..=u16::MAX)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arbitrary_struts() -> impl Strategy<Value = Struts> {
|
||||||
|
(
|
||||||
|
arbitrary_spacing(),
|
||||||
|
arbitrary_spacing(),
|
||||||
|
arbitrary_spacing(),
|
||||||
|
arbitrary_spacing(),
|
||||||
|
)
|
||||||
|
.prop_map(|(left, right, top, bottom)| Struts {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arbitrary_center_focused_column() -> impl Strategy<Value = CenterFocusedColumn> {
|
||||||
|
prop_oneof![
|
||||||
|
Just(CenterFocusedColumn::Never),
|
||||||
|
Just(CenterFocusedColumn::OnOverflow),
|
||||||
|
Just(CenterFocusedColumn::Always),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_compose! {
|
||||||
|
fn arbitrary_focus_ring()(
|
||||||
|
off in any::<bool>(),
|
||||||
|
width in arbitrary_spacing(),
|
||||||
|
) -> niri_config::FocusRing {
|
||||||
|
niri_config::FocusRing {
|
||||||
|
off,
|
||||||
|
width,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_compose! {
|
||||||
|
fn arbitrary_options()(
|
||||||
|
gaps in arbitrary_spacing(),
|
||||||
|
struts in arbitrary_struts(),
|
||||||
|
focus_ring in arbitrary_focus_ring(),
|
||||||
|
border in arbitrary_focus_ring(),
|
||||||
|
center_focused_column in arbitrary_center_focused_column(),
|
||||||
|
) -> Options {
|
||||||
|
Options {
|
||||||
|
gaps: gaps.into(),
|
||||||
|
struts,
|
||||||
|
center_focused_column,
|
||||||
|
focus_ring,
|
||||||
|
border,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
@@ -2202,13 +2464,7 @@ mod tests {
|
|||||||
})]
|
})]
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn random_operations_dont_panic(ops: Vec<Op>, border in arbitrary_border()) {
|
fn random_operations_dont_panic(ops: Vec<Op>, options in arbitrary_options()) {
|
||||||
let mut options = Options::default();
|
|
||||||
if border != 0 {
|
|
||||||
options.border.off = false;
|
|
||||||
options.border.width = border;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eprintln!("{ops:?}");
|
// eprintln!("{ops:?}");
|
||||||
check_ops_with_options(options, &ops);
|
check_ops_with_options(options, &ops);
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-3
@@ -2,6 +2,7 @@ use std::cmp::min;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri_config::SizeChange;
|
||||||
use smithay::backend::renderer::element::utils::{
|
use smithay::backend::renderer::element::utils::{
|
||||||
CropRenderElement, Relocate, RelocateRenderElement,
|
CropRenderElement, Relocate, RelocateRenderElement,
|
||||||
};
|
};
|
||||||
@@ -11,11 +12,10 @@ use smithay::output::Output;
|
|||||||
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
||||||
|
|
||||||
use super::workspace::{
|
use super::workspace::{
|
||||||
compute_working_area, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
|
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
|
||||||
};
|
};
|
||||||
use super::{LayoutElement, Options};
|
use super::{LayoutElement, Options};
|
||||||
use crate::animation::Animation;
|
use crate::animation::Animation;
|
||||||
use crate::config::SizeChange;
|
|
||||||
use crate::utils::output_size;
|
use crate::utils::output_size;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -127,7 +127,26 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clean_up_workspaces(&mut self) {
|
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());
|
assert!(self.workspace_switch.is_none());
|
||||||
|
|
||||||
for idx in (0..self.workspaces.len() - 1).rev() {
|
for idx in (0..self.workspaces.len() - 1).rev() {
|
||||||
@@ -323,6 +342,62 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
self.clean_up_workspaces();
|
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) {
|
pub fn switch_workspace_up(&mut self) {
|
||||||
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
|
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
|
||||||
}
|
}
|
||||||
|
|||||||
+170
-43
@@ -3,6 +3,7 @@ use std::iter::zip;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri_config::{CenterFocusedColumn, PresetWidth, SizeChange, Struts};
|
||||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||||
use smithay::backend::renderer::element::utils::RelocateRenderElement;
|
use smithay::backend::renderer::element::utils::RelocateRenderElement;
|
||||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
use smithay::backend::renderer::{ImportAll, Renderer};
|
||||||
@@ -17,7 +18,6 @@ use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
|||||||
use super::tile::Tile;
|
use super::tile::Tile;
|
||||||
use super::{LayoutElement, Options};
|
use super::{LayoutElement, Options};
|
||||||
use crate::animation::Animation;
|
use crate::animation::Animation;
|
||||||
use crate::config::{PresetWidth, SizeChange, Struts};
|
|
||||||
use crate::utils::output_size;
|
use crate::utils::output_size;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -308,7 +308,7 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
|
|
||||||
fn enter_output_for_window(&self, window: &W) {
|
fn enter_output_for_window(&self, window: &W) {
|
||||||
if let Some(output) = &self.output {
|
if let Some(output) = &self.output {
|
||||||
prepare_for_output(window, output);
|
set_preferred_scale_transform(window, output);
|
||||||
window.output_enter(output);
|
window.output_enter(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,6 +330,15 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_output_scale_transform(&mut self) {
|
||||||
|
let Some(output) = self.output.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for window in self.windows() {
|
||||||
|
set_preferred_scale_transform(window, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn toplevel_bounds(&self) -> Size<i32, Logical> {
|
fn toplevel_bounds(&self) -> Size<i32, Logical> {
|
||||||
let mut border = 0;
|
let mut border = 0;
|
||||||
if !self.options.border.off {
|
if !self.options.border.off {
|
||||||
@@ -363,7 +372,7 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
let bounds = self.toplevel_bounds();
|
let bounds = self.toplevel_bounds();
|
||||||
|
|
||||||
if let Some(output) = self.output.as_ref() {
|
if let Some(output) = self.output.as_ref() {
|
||||||
prepare_for_output(window, output);
|
set_preferred_scale_transform(window, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.toplevel().with_pending_state(|state| {
|
window.toplevel().with_pending_state(|state| {
|
||||||
@@ -397,9 +406,7 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
new_offset - self.working_area.loc.x
|
new_offset - self.working_area.loc.x
|
||||||
}
|
}
|
||||||
|
|
||||||
fn animate_view_offset_to_column(&mut self, current_x: i32, idx: usize) {
|
fn animate_view_offset(&mut self, current_x: i32, idx: usize, new_view_offset: i32) {
|
||||||
let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx);
|
|
||||||
|
|
||||||
let new_col_x = self.column_x(idx);
|
let new_col_x = self.column_x(idx);
|
||||||
let from_view_offset = current_x - new_col_x;
|
let from_view_offset = current_x - new_col_x;
|
||||||
self.view_offset = from_view_offset;
|
self.view_offset = from_view_offset;
|
||||||
@@ -426,13 +433,78 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn animate_view_offset_to_column(&mut self, current_x: i32, idx: usize) {
|
||||||
|
let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx);
|
||||||
|
self.animate_view_offset(current_x, idx, new_view_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animate_view_offset_to_column_centered(&mut self, current_x: i32, idx: usize) {
|
||||||
|
if self.columns.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let col = &self.columns[idx];
|
||||||
|
if col.is_fullscreen {
|
||||||
|
self.animate_view_offset_to_column(current_x, idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = col.width();
|
||||||
|
|
||||||
|
// If the column is wider than the working area, then on commit it will be shifted to left
|
||||||
|
// edge alignment by the usual positioning code, so there's no use in trying to center it
|
||||||
|
// here.
|
||||||
|
if self.working_area.size.w <= width {
|
||||||
|
self.animate_view_offset_to_column(current_x, idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_view_offset = -(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
|
||||||
|
|
||||||
|
self.animate_view_offset(current_x, idx, new_view_offset);
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_column(&mut self, idx: usize) {
|
fn activate_column(&mut self, idx: usize) {
|
||||||
if self.active_column_idx == idx {
|
if self.active_column_idx == idx {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_x = self.view_pos();
|
let current_x = self.view_pos();
|
||||||
|
match self.options.center_focused_column {
|
||||||
|
CenterFocusedColumn::Always => {
|
||||||
|
self.animate_view_offset_to_column_centered(current_x, idx)
|
||||||
|
}
|
||||||
|
CenterFocusedColumn::OnOverflow => {
|
||||||
|
// Always take the left or right neighbor of the target as the source.
|
||||||
|
let source_idx = if self.active_column_idx > idx {
|
||||||
|
min(idx + 1, self.columns.len() - 1)
|
||||||
|
} else {
|
||||||
|
idx.saturating_sub(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let source_x = self.column_x(source_idx);
|
||||||
|
let source_width = self.columns[source_idx].width();
|
||||||
|
|
||||||
|
let target_x = self.column_x(idx);
|
||||||
|
let target_width = self.columns[idx].width();
|
||||||
|
|
||||||
|
let total_width = if source_x < target_x {
|
||||||
|
// Source is left from target.
|
||||||
|
target_x - source_x + target_width
|
||||||
|
} else {
|
||||||
|
// Source is right from target.
|
||||||
|
source_x - target_x + source_width
|
||||||
|
} + self.options.gaps * 2;
|
||||||
|
|
||||||
|
// If it fits together, do a normal animation, otherwise center the new column.
|
||||||
|
if total_width <= self.working_area.size.w {
|
||||||
self.animate_view_offset_to_column(current_x, idx);
|
self.animate_view_offset_to_column(current_x, idx);
|
||||||
|
} else {
|
||||||
|
self.animate_view_offset_to_column_centered(current_x, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CenterFocusedColumn::Never => self.animate_view_offset_to_column(current_x, idx),
|
||||||
|
};
|
||||||
|
|
||||||
self.active_column_idx = idx;
|
self.active_column_idx = idx;
|
||||||
|
|
||||||
@@ -488,15 +560,58 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
width,
|
width,
|
||||||
is_full_width,
|
is_full_width,
|
||||||
);
|
);
|
||||||
|
let width = column.width();
|
||||||
self.columns.insert(idx, column);
|
self.columns.insert(idx, column);
|
||||||
|
|
||||||
if activate {
|
if activate {
|
||||||
// If this is the first window on an empty workspace, skip the animation from whatever
|
// If this is the first window on an empty workspace, skip the animation from whatever
|
||||||
// view_offset was left over.
|
// view_offset was left over.
|
||||||
if was_empty {
|
if was_empty {
|
||||||
|
if self.options.center_focused_column == CenterFocusedColumn::Always {
|
||||||
|
self.view_offset =
|
||||||
|
-(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
|
||||||
|
} else {
|
||||||
// Try to make the code produce a left-aligned offset, even in presence of left
|
// Try to make the code produce a left-aligned offset, even in presence of left
|
||||||
// exclusive zones.
|
// exclusive zones.
|
||||||
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
|
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
|
||||||
|
}
|
||||||
|
self.view_offset_anim = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activate_column(idx);
|
||||||
|
self.activate_prev_column_on_removal = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_column(&mut self, mut column: Column<W>, activate: bool) {
|
||||||
|
for tile in &column.tiles {
|
||||||
|
self.enter_output_for_window(tile.window());
|
||||||
|
}
|
||||||
|
|
||||||
|
let was_empty = self.columns.is_empty();
|
||||||
|
|
||||||
|
let idx = if self.columns.is_empty() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.active_column_idx + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
column.set_view_size(self.view_size, self.working_area);
|
||||||
|
let width = column.width();
|
||||||
|
self.columns.insert(idx, column);
|
||||||
|
|
||||||
|
if activate {
|
||||||
|
// If this is the first window on an empty workspace, skip the animation from whatever
|
||||||
|
// view_offset was left over.
|
||||||
|
if was_empty {
|
||||||
|
if self.options.center_focused_column == CenterFocusedColumn::Always {
|
||||||
|
self.view_offset =
|
||||||
|
-(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
|
||||||
|
} else {
|
||||||
|
// Try to make the code produce a left-aligned offset, even in presence of left
|
||||||
|
// exclusive zones.
|
||||||
|
self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
|
||||||
|
}
|
||||||
self.view_offset_anim = None;
|
self.view_offset_anim = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,6 +664,42 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
window
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_column_by_idx(&mut self, column_idx: usize) -> Column<W> {
|
||||||
|
let column = self.columns.remove(column_idx);
|
||||||
|
|
||||||
|
if let Some(output) = &self.output {
|
||||||
|
for tile in &column.tiles {
|
||||||
|
tile.window().output_leave(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if column_idx + 1 == self.active_column_idx {
|
||||||
|
// The previous column, that we were going to activate upon removal of the active
|
||||||
|
// column, has just been itself removed.
|
||||||
|
self.activate_prev_column_on_removal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: activate_column below computes current view position to compute the new view
|
||||||
|
// position, which can include the column we're removing here. This leads to unwanted
|
||||||
|
// view jumps.
|
||||||
|
if self.columns.is_empty() {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.active_column_idx > column_idx
|
||||||
|
|| (self.active_column_idx == column_idx && self.activate_prev_column_on_removal)
|
||||||
|
{
|
||||||
|
// A column to the left was removed; preserve the current position.
|
||||||
|
// FIXME: preserve activate_prev_column_on_removal.
|
||||||
|
// Or, the active column was removed, and we needed to activate the previous column.
|
||||||
|
self.activate_column(self.active_column_idx.saturating_sub(1));
|
||||||
|
} else {
|
||||||
|
self.activate_column(min(self.active_column_idx, self.columns.len() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
column
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_window(&mut self, window: &W) {
|
pub fn remove_window(&mut self, window: &W) {
|
||||||
let column_idx = self
|
let column_idx = self
|
||||||
.columns
|
.columns
|
||||||
@@ -574,9 +725,16 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
if idx == self.active_column_idx {
|
if idx == self.active_column_idx {
|
||||||
// We might need to move the view to ensure the resized window is still visible.
|
// We might need to move the view to ensure the resized window is still visible.
|
||||||
let current_x = self.view_pos();
|
let current_x = self.view_pos();
|
||||||
|
|
||||||
|
if self.options.center_focused_column == CenterFocusedColumn::Always {
|
||||||
|
// FIXME: we will want to skip the animation in some cases here to make
|
||||||
|
// continuously resizing windows not look janky.
|
||||||
|
self.animate_view_offset_to_column_centered(current_x, idx);
|
||||||
|
} else {
|
||||||
self.animate_view_offset_to_column(current_x, idx);
|
self.animate_view_offset_to_column(current_x, idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn activate_window(&mut self, window: &W) {
|
pub fn activate_window(&mut self, window: &W) {
|
||||||
let column_idx = self
|
let column_idx = self
|
||||||
@@ -654,6 +812,7 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
let column = self.columns.remove(self.active_column_idx);
|
let column = self.columns.remove(self.active_column_idx);
|
||||||
self.columns.insert(new_idx, column);
|
self.columns.insert(new_idx, column);
|
||||||
|
|
||||||
|
// FIXME: should this be different when always centering?
|
||||||
self.view_offset =
|
self.view_offset =
|
||||||
self.compute_new_view_offset_for_column(current_x, self.active_column_idx);
|
self.compute_new_view_offset_for_column(current_x, self.active_column_idx);
|
||||||
|
|
||||||
@@ -714,6 +873,7 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
|
|
||||||
let source_column_idx = self.active_column_idx + 1;
|
let source_column_idx = self.active_column_idx + 1;
|
||||||
let window = self.remove_window_by_idx(source_column_idx, 0);
|
let window = self.remove_window_by_idx(source_column_idx, 0);
|
||||||
|
self.enter_output_for_window(&window);
|
||||||
|
|
||||||
let target_column = &mut self.columns[self.active_column_idx];
|
let target_column = &mut self.columns[self.active_column_idx];
|
||||||
target_column.add_window(window);
|
target_column.add_window(window);
|
||||||
@@ -738,42 +898,8 @@ impl<W: LayoutElement> Workspace<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn center_column(&mut self) {
|
pub fn center_column(&mut self) {
|
||||||
if self.columns.is_empty() {
|
let center_x = self.view_pos();
|
||||||
return;
|
self.animate_view_offset_to_column_centered(center_x, self.active_column_idx);
|
||||||
}
|
|
||||||
|
|
||||||
let col = &self.columns[self.active_column_idx];
|
|
||||||
if col.is_fullscreen {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let width = col.width();
|
|
||||||
|
|
||||||
// If the column is wider than the working area, then on commit it will be shifted to left
|
|
||||||
// edge alignment by the usual positioning code, so there's no use in doing anything here.
|
|
||||||
if self.working_area.size.w <= width {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_view_offset = -(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
|
|
||||||
|
|
||||||
// If we're already animating towards that, don't restart it.
|
|
||||||
if let Some(anim) = &self.view_offset_anim {
|
|
||||||
if anim.to().round() as i32 == new_view_offset {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If our view offset is already this, we don't need to do anything.
|
|
||||||
if self.view_offset == new_view_offset {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.view_offset_anim = Some(Animation::new(
|
|
||||||
self.view_offset as f64,
|
|
||||||
new_view_offset as f64,
|
|
||||||
Duration::from_millis(250),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_pos(&self) -> i32 {
|
fn view_pos(&self) -> i32 {
|
||||||
@@ -1509,7 +1635,8 @@ fn compute_new_view_offset(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_for_output(window: &impl LayoutElement, output: &Output) {
|
fn set_preferred_scale_transform(window: &impl LayoutElement, output: &Output) {
|
||||||
|
// FIXME: cache this on the workspace.
|
||||||
let scale = output.current_scale().integer_scale();
|
let scale = output.current_scale().integer_scale();
|
||||||
let transform = output.current_transform();
|
let transform = output.current_transform();
|
||||||
window.set_preferred_scale_transform(scale, transform);
|
window.set_preferred_scale_transform(scale, transform);
|
||||||
|
|||||||
+78
-16
@@ -3,13 +3,16 @@ 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 render_helpers;
|
||||||
@@ -28,12 +31,12 @@ use std::process::Command;
|
|||||||
use std::{env, mem};
|
use std::{env, mem};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
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};
|
||||||
@@ -42,10 +45,11 @@ 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(args_conflicts_with_subcommands = true)]
|
||||||
#[command(subcommand_value_name = "SUBCOMMAND")]
|
#[command(subcommand_value_name = "SUBCOMMAND")]
|
||||||
#[command(subcommand_help_heading = "Subcommands")]
|
#[command(subcommand_help_heading = "Subcommands")]
|
||||||
@@ -69,6 +73,22 @@ enum Sub {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
config: Option<PathBuf>,
|
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>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -110,33 +130,45 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _client = tracy_client::Client::start();
|
let _client = tracy_client::Client::start();
|
||||||
|
|
||||||
// Set a better error printer for config loading.
|
// Set a better error printer for config loading.
|
||||||
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))).unwrap();
|
niri_config::set_miette_hook().unwrap();
|
||||||
|
|
||||||
// Handle subcommands.
|
// Handle subcommands.
|
||||||
if let Some(subcommand) = cli.subcommand {
|
if let Some(subcommand) = cli.subcommand {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
Sub::Validate { config } => {
|
Sub::Validate { config } => {
|
||||||
Config::load(config).context("error loading config")?;
|
let path = config
|
||||||
|
.or_else(default_config_path)
|
||||||
|
.expect("error getting config path");
|
||||||
|
Config::load(&path)?;
|
||||||
info!("config is valid");
|
info!("config is valid");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
Sub::Msg { msg, json } => {
|
||||||
|
handle_msg(msg, json)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Sub::Panic => cause_panic(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!("starting version {}", &version());
|
||||||
"starting version {} ({})",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
git_version!(fallback = "unknown commit"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load the config.
|
// Load the config.
|
||||||
let (mut config, path) = match Config::load(cli.config).context("error loading config") {
|
let path = cli.config.or_else(default_config_path);
|
||||||
Ok((config, path)) => (config, Some(path)),
|
|
||||||
|
let mut config_errored = false;
|
||||||
|
let mut config = path
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|path| match Config::load(path) {
|
||||||
|
Ok(config) => Some(config),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("{err:?}");
|
warn!("{err:?}");
|
||||||
(Config::default(), None)
|
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);
|
||||||
|
|
||||||
@@ -158,6 +190,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
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();
|
||||||
@@ -202,6 +240,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
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())
|
||||||
@@ -210,6 +253,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn version() -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
git_version!(fallback = "unknown commit"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn import_env_to_systemd() {
|
fn import_env_to_systemd() {
|
||||||
let rv = Command::new("/bin/sh")
|
let rv = Command::new("/bin/sh")
|
||||||
.args([
|
.args([
|
||||||
@@ -237,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)
|
||||||
|
}
|
||||||
|
|||||||
+396
-49
@@ -10,6 +10,8 @@ use std::{env, mem, thread};
|
|||||||
|
|
||||||
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode;
|
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use calloop::futures::Scheduler;
|
||||||
|
use niri_config::{Config, TrackLayout};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use smithay::backend::renderer::element::surface::{
|
use smithay::backend::renderer::element::surface::{
|
||||||
@@ -34,12 +36,13 @@ use smithay::desktop::utils::{
|
|||||||
under_from_surface_tree, update_surface_primary_scanout_output, OutputPresentationFeedback,
|
under_from_surface_tree, update_surface_primary_scanout_output, OutputPresentationFeedback,
|
||||||
};
|
};
|
||||||
use smithay::desktop::{
|
use smithay::desktop::{
|
||||||
layer_map_for_output, LayerSurface, PopupManager, Space, Window, WindowSurfaceType,
|
layer_map_for_output, LayerSurface, PopupGrab, PopupManager, PopupUngrabStrategy, Space,
|
||||||
|
Window, WindowSurfaceType,
|
||||||
};
|
};
|
||||||
use smithay::input::keyboard::{Layout as KeyboardLayout, XkbContextHandler};
|
use smithay::input::keyboard::{Layout as KeyboardLayout, XkbContextHandler};
|
||||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus, MotionEvent};
|
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus, MotionEvent};
|
||||||
use smithay::input::{Seat, SeatState};
|
use smithay::input::{Seat, SeatState};
|
||||||
use smithay::output::Output;
|
use smithay::output::{self, Output};
|
||||||
use smithay::reexports::calloop::generic::Generic;
|
use smithay::reexports::calloop::generic::Generic;
|
||||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||||
use smithay::reexports::calloop::{
|
use smithay::reexports::calloop::{
|
||||||
@@ -69,6 +72,7 @@ use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerCons
|
|||||||
use smithay::wayland::pointer_gestures::PointerGesturesState;
|
use smithay::wayland::pointer_gestures::PointerGesturesState;
|
||||||
use smithay::wayland::presentation::PresentationState;
|
use smithay::wayland::presentation::PresentationState;
|
||||||
use smithay::wayland::relative_pointer::RelativePointerManagerState;
|
use smithay::wayland::relative_pointer::RelativePointerManagerState;
|
||||||
|
use smithay::wayland::security_context::SecurityContextState;
|
||||||
use smithay::wayland::selection::data_device::{set_data_device_selection, DataDeviceState};
|
use smithay::wayland::selection::data_device::{set_data_device_selection, DataDeviceState};
|
||||||
use smithay::wayland::selection::primary_selection::PrimarySelectionState;
|
use smithay::wayland::selection::primary_selection::PrimarySelectionState;
|
||||||
use smithay::wayland::selection::wlr_data_control::DataControlState;
|
use smithay::wayland::selection::wlr_data_control::DataControlState;
|
||||||
@@ -86,15 +90,20 @@ use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState;
|
|||||||
use crate::animation;
|
use crate::animation;
|
||||||
use crate::backend::tty::{SurfaceDmabufFeedback, TtyFrame, TtyRenderer, TtyRendererError};
|
use crate::backend::tty::{SurfaceDmabufFeedback, TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
use crate::backend::{Backend, RenderResult, Tty, Winit};
|
use crate::backend::{Backend, RenderResult, Tty, Winit};
|
||||||
use crate::config::{Config, TrackLayout};
|
use crate::config_error_notification::{
|
||||||
|
ConfigErrorNotification, ConfigErrorNotificationRenderElement,
|
||||||
|
};
|
||||||
use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor};
|
use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor};
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
|
use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
|
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
|
||||||
|
use crate::exit_confirm_dialog::ExitConfirmDialog;
|
||||||
use crate::frame_clock::FrameClock;
|
use crate::frame_clock::FrameClock;
|
||||||
use crate::handlers::configure_lock_surface;
|
use crate::handlers::configure_lock_surface;
|
||||||
use crate::input::TabletData;
|
use crate::hotkey_overlay::HotkeyOverlay;
|
||||||
|
use crate::input::{apply_libinput_settings, TabletData};
|
||||||
|
use crate::ipc::server::IpcServer;
|
||||||
use crate::layout::{Layout, MonitorRenderElement};
|
use crate::layout::{Layout, MonitorRenderElement};
|
||||||
use crate::pw_utils::{Cast, PipeWire};
|
use crate::pw_utils::{Cast, PipeWire};
|
||||||
use crate::render_helpers::{NiriRenderer, PrimaryGpuTextureRenderElement};
|
use crate::render_helpers::{NiriRenderer, PrimaryGpuTextureRenderElement};
|
||||||
@@ -110,6 +119,7 @@ pub struct Niri {
|
|||||||
pub config: Rc<RefCell<Config>>,
|
pub config: Rc<RefCell<Config>>,
|
||||||
|
|
||||||
pub event_loop: LoopHandle<'static, State>,
|
pub event_loop: LoopHandle<'static, State>,
|
||||||
|
pub scheduler: Scheduler<()>,
|
||||||
pub stop_signal: LoopSignal,
|
pub stop_signal: LoopSignal,
|
||||||
pub display_handle: DisplayHandle,
|
pub display_handle: DisplayHandle,
|
||||||
pub socket_name: OsString,
|
pub socket_name: OsString,
|
||||||
@@ -133,6 +143,7 @@ pub struct Niri {
|
|||||||
// When false, we're idling with monitors powered off.
|
// When false, we're idling with monitors powered off.
|
||||||
pub monitors_active: bool,
|
pub monitors_active: bool,
|
||||||
|
|
||||||
|
pub devices: HashSet<input::Device>,
|
||||||
pub tablets: HashMap<input::Device, TabletData>,
|
pub tablets: HashMap<input::Device, TabletData>,
|
||||||
|
|
||||||
// Smithay state.
|
// Smithay state.
|
||||||
@@ -157,11 +168,17 @@ pub struct Niri {
|
|||||||
pub primary_selection_state: PrimarySelectionState,
|
pub primary_selection_state: PrimarySelectionState,
|
||||||
pub data_control_state: DataControlState,
|
pub data_control_state: DataControlState,
|
||||||
pub popups: PopupManager,
|
pub popups: PopupManager,
|
||||||
|
pub popup_grab: Option<PopupGrabState>,
|
||||||
pub presentation_state: PresentationState,
|
pub presentation_state: PresentationState,
|
||||||
|
pub security_context_state: SecurityContextState,
|
||||||
|
|
||||||
pub seat: Seat<State>,
|
pub seat: Seat<State>,
|
||||||
/// Scancodes of the keys to suppress.
|
/// Scancodes of the keys to suppress.
|
||||||
pub suppressed_keys: HashSet<u32>,
|
pub suppressed_keys: HashSet<u32>,
|
||||||
|
// This is always a toplevel surface focused as far as niri's logic is concerned, even when
|
||||||
|
// popup grabs are active (which means the real keyboard focus is on a popup descending from
|
||||||
|
// this toplevel surface).
|
||||||
|
pub keyboard_focus: Option<WlSurface>,
|
||||||
|
|
||||||
pub cursor_manager: CursorManager,
|
pub cursor_manager: CursorManager,
|
||||||
pub cursor_texture_cache: CursorTextureCache,
|
pub cursor_texture_cache: CursorTextureCache,
|
||||||
@@ -173,12 +190,17 @@ pub struct Niri {
|
|||||||
pub lock_state: LockState,
|
pub lock_state: LockState,
|
||||||
|
|
||||||
pub screenshot_ui: ScreenshotUi,
|
pub screenshot_ui: ScreenshotUi,
|
||||||
|
pub config_error_notification: ConfigErrorNotification,
|
||||||
|
pub hotkey_overlay: HotkeyOverlay,
|
||||||
|
pub exit_confirm_dialog: Option<ExitConfirmDialog>,
|
||||||
|
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
pub dbus: Option<crate::dbus::DBusServers>,
|
pub dbus: Option<crate::dbus::DBusServers>,
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
pub inhibit_power_key_fd: Option<zbus::zvariant::OwnedFd>,
|
pub inhibit_power_key_fd: Option<zbus::zvariant::OwnedFd>,
|
||||||
|
|
||||||
|
pub ipc_server: Option<IpcServer>,
|
||||||
|
|
||||||
// Casts are dropped before PipeWire to prevent a double-free (yay).
|
// Casts are dropped before PipeWire to prevent a double-free (yay).
|
||||||
pub casts: Vec<Cast>,
|
pub casts: Vec<Cast>,
|
||||||
pub pipewire: Option<PipeWire>,
|
pub pipewire: Option<PipeWire>,
|
||||||
@@ -222,6 +244,11 @@ pub enum RedrawState {
|
|||||||
WaitingForEstimatedVBlankAndQueued((RegistrationToken, Idle<'static>)),
|
WaitingForEstimatedVBlankAndQueued((RegistrationToken, Idle<'static>)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PopupGrabState {
|
||||||
|
pub root: WlSurface,
|
||||||
|
pub grab: PopupGrab<State>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct PointerFocus {
|
pub struct PointerFocus {
|
||||||
pub output: Output,
|
pub output: Output,
|
||||||
@@ -299,7 +326,8 @@ impl State {
|
|||||||
self.niri.cursor_manager.check_cursor_image_surface_alive();
|
self.niri.cursor_manager.check_cursor_image_surface_alive();
|
||||||
self.niri.refresh_pointer_outputs();
|
self.niri.refresh_pointer_outputs();
|
||||||
self.niri.popups.cleanup();
|
self.niri.popups.cleanup();
|
||||||
self.update_focus();
|
self.niri.refresh_popup_grab();
|
||||||
|
self.update_keyboard_focus();
|
||||||
self.refresh_pointer_focus();
|
self.refresh_pointer_focus();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -392,7 +420,7 @@ impl State {
|
|||||||
self.move_cursor(center(geo).to_f64());
|
self.move_cursor(center(geo).to_f64());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_focus(&mut self) {
|
pub fn update_keyboard_focus(&mut self) {
|
||||||
let focus = if self.niri.is_locked() {
|
let focus = if self.niri.is_locked() {
|
||||||
self.niri.lock_surface_focus()
|
self.niri.lock_surface_focus()
|
||||||
} else if self.niri.screenshot_ui.is_open() {
|
} else if self.niri.screenshot_ui.is_open() {
|
||||||
@@ -401,6 +429,17 @@ impl State {
|
|||||||
let mon = self.niri.layout.monitor_for_output(output).unwrap();
|
let mon = self.niri.layout.monitor_for_output(output).unwrap();
|
||||||
let layers = layer_map_for_output(output);
|
let layers = layer_map_for_output(output);
|
||||||
|
|
||||||
|
// Explicitly check for layer-shell popup grabs here, our keyboard focus will stay on
|
||||||
|
// the root layer surface while it has grabs.
|
||||||
|
let layer_grab = self.niri.popup_grab.as_ref().and_then(|g| {
|
||||||
|
layers
|
||||||
|
.layer_for_surface(&g.root, WindowSurfaceType::TOPLEVEL)
|
||||||
|
.map(|l| (&g.root, l.layer()))
|
||||||
|
});
|
||||||
|
let grab_on_layer = |layer: Layer| {
|
||||||
|
layer_grab.and_then(move |(s, l)| if l == layer { Some(s.clone()) } else { None })
|
||||||
|
};
|
||||||
|
|
||||||
let layout_focus = || {
|
let layout_focus = || {
|
||||||
self.niri
|
self.niri
|
||||||
.layout
|
.layout
|
||||||
@@ -413,7 +452,14 @@ impl State {
|
|||||||
.then(|| surface.wl_surface().clone())
|
.then(|| surface.wl_surface().clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut surface = layers.layers_on(Layer::Overlay).find_map(layer_focus);
|
let mut surface = grab_on_layer(Layer::Overlay);
|
||||||
|
// FIXME: we shouldn't prioritize the top layer grabs over regular overlay input or a
|
||||||
|
// fullscreen layout window. This will need tracking in grab() to avoid handing it out
|
||||||
|
// in the first place. Or a better way to structure this code.
|
||||||
|
surface = surface.or_else(|| grab_on_layer(Layer::Top));
|
||||||
|
|
||||||
|
surface = surface.or_else(|| layers.layers_on(Layer::Overlay).find_map(layer_focus));
|
||||||
|
|
||||||
if mon.render_above_top_layer() {
|
if mon.render_above_top_layer() {
|
||||||
surface = surface.or_else(layout_focus);
|
surface = surface.or_else(layout_focus);
|
||||||
surface = surface.or_else(|| layers.layers_on(Layer::Top).find_map(layer_focus));
|
surface = surface.or_else(|| layers.layers_on(Layer::Top).find_map(layer_focus));
|
||||||
@@ -428,15 +474,39 @@ impl State {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||||
let current_focus = keyboard.current_focus();
|
if self.niri.keyboard_focus != focus {
|
||||||
if current_focus != focus {
|
trace!(
|
||||||
|
"keyboard focus changed from {:?} to {:?}",
|
||||||
|
self.niri.keyboard_focus,
|
||||||
|
focus
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(grab) = self.niri.popup_grab.as_mut() {
|
||||||
|
if Some(&grab.root) != focus.as_ref() {
|
||||||
|
trace!(
|
||||||
|
"grab root {:?} is not the new focus {:?}, ungrabbing",
|
||||||
|
grab.root,
|
||||||
|
focus
|
||||||
|
);
|
||||||
|
|
||||||
|
grab.grab.ungrab(PopupUngrabStrategy::All);
|
||||||
|
keyboard.unset_grab();
|
||||||
|
self.niri.seat.get_pointer().unwrap().unset_grab(
|
||||||
|
self,
|
||||||
|
SERIAL_COUNTER.next_serial(),
|
||||||
|
get_monotonic_time().as_millis() as u32,
|
||||||
|
);
|
||||||
|
self.niri.popup_grab = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.niri.config.borrow().input.keyboard.track_layout == TrackLayout::Window {
|
if self.niri.config.borrow().input.keyboard.track_layout == TrackLayout::Window {
|
||||||
let current_layout =
|
let current_layout =
|
||||||
keyboard.with_xkb_state(self, |context| context.active_layout());
|
keyboard.with_xkb_state(self, |context| context.active_layout());
|
||||||
|
|
||||||
let mut new_layout = current_layout;
|
let mut new_layout = current_layout;
|
||||||
// Store the currently active layout for the surface.
|
// Store the currently active layout for the surface.
|
||||||
if let Some(current_focus) = current_focus.as_ref() {
|
if let Some(current_focus) = self.niri.keyboard_focus.as_ref() {
|
||||||
with_states(current_focus, |data| {
|
with_states(current_focus, |data| {
|
||||||
let cell = data
|
let cell = data
|
||||||
.data_map
|
.data_map
|
||||||
@@ -463,6 +533,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.niri.keyboard_focus = focus.clone();
|
||||||
keyboard.set_focus(self, focus, SERIAL_COUNTER.next_serial());
|
keyboard.set_focus(self, focus, SERIAL_COUNTER.next_serial());
|
||||||
|
|
||||||
// FIXME: can be more granular.
|
// FIXME: can be more granular.
|
||||||
@@ -473,18 +544,24 @@ impl State {
|
|||||||
pub fn reload_config(&mut self, path: PathBuf) {
|
pub fn reload_config(&mut self, path: PathBuf) {
|
||||||
let _span = tracy_client::span!("State::reload_config");
|
let _span = tracy_client::span!("State::reload_config");
|
||||||
|
|
||||||
let config = match Config::load(Some(path)) {
|
let config = match Config::load(&path) {
|
||||||
Ok((config, _)) => config,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("{:?}", err.context("error loading config"));
|
warn!("{:?}", err.context("error loading config"));
|
||||||
|
self.niri.config_error_notification.show();
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.niri.config_error_notification.hide();
|
||||||
|
|
||||||
self.niri.layout.update_config(&config);
|
self.niri.layout.update_config(&config);
|
||||||
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
|
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
|
||||||
|
|
||||||
let mut reload_xkb = None;
|
let mut reload_xkb = None;
|
||||||
|
let mut libinput_config_changed = false;
|
||||||
|
let mut output_config_changed = false;
|
||||||
let mut old_config = self.niri.config.borrow_mut();
|
let mut old_config = self.niri.config.borrow_mut();
|
||||||
|
|
||||||
// Reload the cursor.
|
// Reload the cursor.
|
||||||
@@ -511,6 +588,20 @@ impl State {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.input.touchpad != old_config.input.touchpad
|
||||||
|
|| config.input.mouse != old_config.input.mouse
|
||||||
|
{
|
||||||
|
libinput_config_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.outputs != old_config.outputs {
|
||||||
|
output_config_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.binds != old_config.binds {
|
||||||
|
self.niri.hotkey_overlay.on_hotkey_config_updated();
|
||||||
|
}
|
||||||
|
|
||||||
*old_config = config;
|
*old_config = config;
|
||||||
|
|
||||||
// Release the borrow.
|
// Release the borrow.
|
||||||
@@ -524,10 +615,52 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if libinput_config_changed {
|
||||||
|
let config = self.niri.config.borrow();
|
||||||
|
for mut device in self.niri.devices.iter().cloned() {
|
||||||
|
apply_libinput_settings(&config.input, &mut device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if output_config_changed {
|
||||||
|
let mut resized_outputs = vec![];
|
||||||
|
for output in self.niri.global_space.outputs() {
|
||||||
|
let name = output.name();
|
||||||
|
let scale = self
|
||||||
|
.niri
|
||||||
|
.config
|
||||||
|
.borrow()
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.name == name)
|
||||||
|
.map(|c| c.scale)
|
||||||
|
.unwrap_or(1.);
|
||||||
|
let scale = scale.clamp(1., 10.).ceil() as i32;
|
||||||
|
if output.current_scale().integer_scale() != scale {
|
||||||
|
output.change_current_state(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(output::Scale::Integer(scale)),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
resized_outputs.push(output.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for output in resized_outputs {
|
||||||
|
self.niri.output_resized(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.niri.reposition_outputs(None);
|
||||||
|
|
||||||
|
self.backend.on_output_config_changed(&mut self.niri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
|
||||||
|
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
|
||||||
|
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
|
||||||
|
// clients will use the new xdg-decoration setting.
|
||||||
|
|
||||||
self.niri.queue_redraw_all();
|
self.niri.queue_redraw_all();
|
||||||
// FIXME: apply output scale and whatnot.
|
|
||||||
// FIXME: apply libinput device settings.
|
|
||||||
// FIXME: apply xdg decoration settings.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
@@ -574,6 +707,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
|
ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
|
||||||
|
ScreenCastToNiri::Redraw(output) => self.niri.queue_redraw(output),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,6 +764,9 @@ impl Niri {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let _span = tracy_client::span!("Niri::new");
|
let _span = tracy_client::span!("Niri::new");
|
||||||
|
|
||||||
|
let (executor, scheduler) = calloop::futures::executor().unwrap();
|
||||||
|
event_loop.insert_source(executor, |_, _, _| ()).unwrap();
|
||||||
|
|
||||||
let display_handle = display.handle();
|
let display_handle = display.handle();
|
||||||
let config_ = config.borrow();
|
let config_ = config.borrow();
|
||||||
|
|
||||||
@@ -640,18 +777,32 @@ impl Niri {
|
|||||||
&display_handle,
|
&display_handle,
|
||||||
[WmCapabilities::Fullscreen],
|
[WmCapabilities::Fullscreen],
|
||||||
);
|
);
|
||||||
let xdg_decoration_state = XdgDecorationState::new::<State>(&display_handle);
|
let xdg_decoration_state =
|
||||||
let kde_decoration_state = KdeDecorationState::new::<State>(
|
XdgDecorationState::new_with_filter::<State, _>(&display_handle, |client| {
|
||||||
|
client
|
||||||
|
.get_data::<ClientState>()
|
||||||
|
.unwrap()
|
||||||
|
.can_view_decoration_globals
|
||||||
|
});
|
||||||
|
let kde_decoration_state = KdeDecorationState::new_with_filter::<State, _>(
|
||||||
&display_handle,
|
&display_handle,
|
||||||
if config_.prefer_no_csd {
|
// If we want CSD we will hide the global.
|
||||||
KdeDecorationsMode::Server
|
KdeDecorationsMode::Server,
|
||||||
} else {
|
|client| {
|
||||||
KdeDecorationsMode::Client
|
client
|
||||||
|
.get_data::<ClientState>()
|
||||||
|
.unwrap()
|
||||||
|
.can_view_decoration_globals
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let layer_shell_state = WlrLayerShellState::new::<State>(&display_handle);
|
let layer_shell_state =
|
||||||
|
WlrLayerShellState::new_with_filter::<State, _>(&display_handle, |client| {
|
||||||
|
!client.get_data::<ClientState>().unwrap().restricted
|
||||||
|
});
|
||||||
let session_lock_state =
|
let session_lock_state =
|
||||||
SessionLockManagerState::new::<State, _>(&display_handle, |_| true);
|
SessionLockManagerState::new::<State, _>(&display_handle, |client| {
|
||||||
|
!client.get_data::<ClientState>().unwrap().restricted
|
||||||
|
});
|
||||||
let shm_state = ShmState::new::<State>(&display_handle, vec![]);
|
let shm_state = ShmState::new::<State>(&display_handle, vec![]);
|
||||||
let output_manager_state =
|
let output_manager_state =
|
||||||
OutputManagerState::new_with_xdg_output::<State>(&display_handle);
|
OutputManagerState::new_with_xdg_output::<State>(&display_handle);
|
||||||
@@ -666,16 +817,24 @@ impl Niri {
|
|||||||
let data_control_state = DataControlState::new::<State, _>(
|
let data_control_state = DataControlState::new::<State, _>(
|
||||||
&display_handle,
|
&display_handle,
|
||||||
Some(&primary_selection_state),
|
Some(&primary_selection_state),
|
||||||
|_| true,
|
|client| !client.get_data::<ClientState>().unwrap().restricted,
|
||||||
);
|
);
|
||||||
let presentation_state =
|
let presentation_state =
|
||||||
PresentationState::new::<State>(&display_handle, Monotonic::ID as u32);
|
PresentationState::new::<State>(&display_handle, Monotonic::ID as u32);
|
||||||
|
let security_context_state =
|
||||||
|
SecurityContextState::new::<State, _>(&display_handle, |client| {
|
||||||
|
!client.get_data::<ClientState>().unwrap().restricted
|
||||||
|
});
|
||||||
|
|
||||||
let text_input_state = TextInputManagerState::new::<State>(&display_handle);
|
let text_input_state = TextInputManagerState::new::<State>(&display_handle);
|
||||||
let input_method_state =
|
let input_method_state =
|
||||||
InputMethodManagerState::new::<State, _>(&display_handle, |_| true);
|
InputMethodManagerState::new::<State, _>(&display_handle, |client| {
|
||||||
|
!client.get_data::<ClientState>().unwrap().restricted
|
||||||
|
});
|
||||||
let virtual_keyboard_state =
|
let virtual_keyboard_state =
|
||||||
VirtualKeyboardManagerState::new::<State, _>(&display_handle, |_| true);
|
VirtualKeyboardManagerState::new::<State, _>(&display_handle, |client| {
|
||||||
|
!client.get_data::<ClientState>().unwrap().restricted
|
||||||
|
});
|
||||||
|
|
||||||
let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, backend.seat_name());
|
let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, backend.seat_name());
|
||||||
seat.add_keyboard(
|
seat.add_keyboard(
|
||||||
@@ -708,18 +867,46 @@ impl Niri {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let screenshot_ui = ScreenshotUi::new();
|
let screenshot_ui = ScreenshotUi::new();
|
||||||
|
let config_error_notification = ConfigErrorNotification::new();
|
||||||
|
|
||||||
|
let mut hotkey_overlay = HotkeyOverlay::new(config.clone(), backend.mod_key());
|
||||||
|
if !config_.hotkey_overlay.skip_at_startup {
|
||||||
|
hotkey_overlay.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
let exit_confirm_dialog = match ExitConfirmDialog::new() {
|
||||||
|
Ok(x) => Some(x),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error creating the exit confirm dialog: {err:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let socket_source = ListeningSocketSource::new_auto().unwrap();
|
let socket_source = ListeningSocketSource::new_auto().unwrap();
|
||||||
let socket_name = socket_source.socket_name().to_os_string();
|
let socket_name = socket_source.socket_name().to_os_string();
|
||||||
event_loop
|
event_loop
|
||||||
.insert_source(socket_source, move |client, _, state| {
|
.insert_source(socket_source, move |client, _, state| {
|
||||||
let data = Arc::new(ClientState::default());
|
let config = state.niri.config.borrow();
|
||||||
|
let data = Arc::new(ClientState {
|
||||||
|
compositor_state: Default::default(),
|
||||||
|
can_view_decoration_globals: config.prefer_no_csd,
|
||||||
|
restricted: false,
|
||||||
|
});
|
||||||
|
|
||||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||||
error!("error inserting client: {err}");
|
error!("error inserting client: {err}");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let ipc_server = match IpcServer::start(&event_loop, &socket_name.to_string_lossy()) {
|
||||||
|
Ok(server) => Some(server),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error starting IPC server: {err:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let pipewire = match PipeWire::new(&event_loop) {
|
let pipewire = match PipeWire::new(&event_loop) {
|
||||||
Ok(pipewire) => Some(pipewire),
|
Ok(pipewire) => Some(pipewire),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -744,6 +931,7 @@ impl Niri {
|
|||||||
config,
|
config,
|
||||||
|
|
||||||
event_loop,
|
event_loop,
|
||||||
|
scheduler,
|
||||||
stop_signal,
|
stop_signal,
|
||||||
socket_name,
|
socket_name,
|
||||||
display_handle,
|
display_handle,
|
||||||
@@ -756,6 +944,7 @@ impl Niri {
|
|||||||
unmapped_windows: HashMap::new(),
|
unmapped_windows: HashMap::new(),
|
||||||
monitors_active: true,
|
monitors_active: true,
|
||||||
|
|
||||||
|
devices: HashSet::new(),
|
||||||
tablets: HashMap::new(),
|
tablets: HashMap::new(),
|
||||||
|
|
||||||
compositor_state,
|
compositor_state,
|
||||||
@@ -779,10 +968,13 @@ impl Niri {
|
|||||||
primary_selection_state,
|
primary_selection_state,
|
||||||
data_control_state,
|
data_control_state,
|
||||||
popups: PopupManager::default(),
|
popups: PopupManager::default(),
|
||||||
|
popup_grab: None,
|
||||||
suppressed_keys: HashSet::new(),
|
suppressed_keys: HashSet::new(),
|
||||||
presentation_state,
|
presentation_state,
|
||||||
|
security_context_state,
|
||||||
|
|
||||||
seat,
|
seat,
|
||||||
|
keyboard_focus: None,
|
||||||
cursor_manager,
|
cursor_manager,
|
||||||
cursor_texture_cache: Default::default(),
|
cursor_texture_cache: Default::default(),
|
||||||
cursor_shape_manager_state,
|
cursor_shape_manager_state,
|
||||||
@@ -793,12 +985,17 @@ impl Niri {
|
|||||||
lock_state: LockState::Unlocked,
|
lock_state: LockState::Unlocked,
|
||||||
|
|
||||||
screenshot_ui,
|
screenshot_ui,
|
||||||
|
config_error_notification,
|
||||||
|
hotkey_overlay,
|
||||||
|
exit_confirm_dialog,
|
||||||
|
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
dbus: None,
|
dbus: None,
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
inhibit_power_key_fd: None,
|
inhibit_power_key_fd: None,
|
||||||
|
|
||||||
|
ipc_server,
|
||||||
|
|
||||||
pipewire,
|
pipewire,
|
||||||
casts: vec![],
|
casts: vec![],
|
||||||
}
|
}
|
||||||
@@ -824,22 +1021,67 @@ impl Niri {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_output(&mut self, output: Output, refresh_interval: Option<Duration>) {
|
/// Repositions all outputs, optionally adding a new output.
|
||||||
let global = output.create_global::<State>(&self.display_handle);
|
pub fn reposition_outputs(&mut self, new_output: Option<&Output>) {
|
||||||
|
let _span = tracy_client::span!("Niri::reposition_outputs");
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Data {
|
||||||
|
output: Output,
|
||||||
|
name: String,
|
||||||
|
position: Option<Point<i32, Logical>>,
|
||||||
|
config: Option<niri_config::Position>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = self.config.borrow();
|
||||||
|
let mut outputs = vec![];
|
||||||
|
for output in self.global_space.outputs().chain(new_output) {
|
||||||
let name = output.name();
|
let name = output.name();
|
||||||
let config = self
|
let position = self.global_space.output_geometry(output).map(|geo| geo.loc);
|
||||||
.config
|
let config = config
|
||||||
.borrow()
|
|
||||||
.outputs
|
.outputs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|o| o.name == name)
|
.find(|o| o.name == name)
|
||||||
.cloned()
|
.and_then(|c| c.position);
|
||||||
.unwrap_or_default();
|
|
||||||
|
outputs.push(Data {
|
||||||
|
output: output.clone(),
|
||||||
|
name,
|
||||||
|
position,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
drop(config);
|
||||||
|
|
||||||
|
for Data { output, .. } in &outputs {
|
||||||
|
self.global_space.unmap_output(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connectors can appear in udev in any order. If we sort by name then we get output
|
||||||
|
// positioning that does not depend on the order they appeared.
|
||||||
|
//
|
||||||
|
// All outputs must have different (connector) names.
|
||||||
|
outputs.sort_unstable_by(|a, b| Ord::cmp(&a.name, &b.name));
|
||||||
|
|
||||||
|
// Place all outputs with explicitly configured position first, then the unconfigured ones.
|
||||||
|
outputs.sort_by_key(|d| d.config.is_none());
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"placing outputs in order: {:?}",
|
||||||
|
outputs.iter().map(|d| &d.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
for data in outputs.into_iter() {
|
||||||
|
let Data {
|
||||||
|
output,
|
||||||
|
name,
|
||||||
|
position,
|
||||||
|
config,
|
||||||
|
} = data;
|
||||||
|
|
||||||
let size = output_size(&output);
|
let size = output_size(&output);
|
||||||
let position = config
|
|
||||||
.position
|
let new_position = config
|
||||||
.map(|pos| Point::from((pos.x, pos.y)))
|
.map(|pos| Point::from((pos.x, pos.y)))
|
||||||
.filter(|pos| {
|
.filter(|pos| {
|
||||||
// Ensure that the requested position does not overlap any existing output.
|
// Ensure that the requested position does not overlap any existing output.
|
||||||
@@ -853,7 +1095,7 @@ impl Niri {
|
|||||||
|
|
||||||
if let Some(overlap) = overlap {
|
if let Some(overlap) = overlap {
|
||||||
warn!(
|
warn!(
|
||||||
"new output {name} at x={} y={} sized {}x{} \
|
"output {name} at x={} y={} sized {}x{} \
|
||||||
overlaps an existing output at x={} y={} sized {}x{}, \
|
overlaps an existing output at x={} y={} sized {}x{}, \
|
||||||
falling back to automatic placement",
|
falling back to automatic placement",
|
||||||
pos.x,
|
pos.x,
|
||||||
@@ -883,13 +1125,39 @@ impl Niri {
|
|||||||
Point::from((x, 0))
|
Point::from((x, 0))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.global_space.map_output(&output, new_position);
|
||||||
|
|
||||||
|
// By passing new_output as an Option, rather than mapping it into a bogus location
|
||||||
|
// in global_space, we ensure that this branch always runs for it.
|
||||||
|
if Some(new_position) != position {
|
||||||
debug!(
|
debug!(
|
||||||
"putting new output {name} at x={} y={}",
|
"putting output {name} at x={} y={}",
|
||||||
position.x, position.y
|
new_position.x, new_position.y
|
||||||
);
|
);
|
||||||
self.global_space.map_output(&output, position);
|
output.change_current_state(None, None, None, Some(new_position));
|
||||||
|
self.queue_redraw(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_output(&mut self, output: Output, refresh_interval: Option<Duration>) {
|
||||||
|
let global = output.create_global::<State>(&self.display_handle);
|
||||||
|
|
||||||
|
let name = output.name();
|
||||||
|
let scale = self
|
||||||
|
.config
|
||||||
|
.borrow()
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.name == name)
|
||||||
|
.map(|c| c.scale)
|
||||||
|
.unwrap_or(1.);
|
||||||
|
let scale = scale.clamp(1., 10.).ceil() as i32;
|
||||||
|
|
||||||
|
// Set scale before adding to the layout since that will read the output size.
|
||||||
|
output.change_current_state(None, None, Some(output::Scale::Integer(scale)), None);
|
||||||
|
|
||||||
self.layout.add_output(output.clone());
|
self.layout.add_output(output.clone());
|
||||||
output.change_current_state(None, None, None, Some(position));
|
|
||||||
|
|
||||||
let lock_render_state = if self.is_locked() {
|
let lock_render_state = if self.is_locked() {
|
||||||
// We haven't rendered anything yet so it's as good as locked.
|
// We haven't rendered anything yet so it's as good as locked.
|
||||||
@@ -898,6 +1166,7 @@ impl Niri {
|
|||||||
LockRenderState::Unlocked
|
LockRenderState::Unlocked
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let size = output_size(&output);
|
||||||
let state = OutputState {
|
let state = OutputState {
|
||||||
global,
|
global,
|
||||||
redraw_state: RedrawState::Idle,
|
redraw_state: RedrawState::Idle,
|
||||||
@@ -911,14 +1180,21 @@ impl Niri {
|
|||||||
};
|
};
|
||||||
let rv = self.output_state.insert(output.clone(), state);
|
let rv = self.output_state.insert(output.clone(), state);
|
||||||
assert!(rv.is_none(), "output was already tracked");
|
assert!(rv.is_none(), "output was already tracked");
|
||||||
let rv = self.output_by_name.insert(name, output);
|
let rv = self.output_by_name.insert(name, output.clone());
|
||||||
assert!(rv.is_none(), "output was already tracked");
|
assert!(rv.is_none(), "output was already tracked");
|
||||||
|
|
||||||
|
// Must be last since it will call queue_redraw(output) which needs things to be filled-in.
|
||||||
|
self.reposition_outputs(Some(&output));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_output(&mut self, output: &Output) {
|
pub fn remove_output(&mut self, output: &Output) {
|
||||||
|
for layer in layer_map_for_output(output).layers() {
|
||||||
|
layer.layer_surface().send_close();
|
||||||
|
}
|
||||||
|
|
||||||
self.layout.remove_output(output);
|
self.layout.remove_output(output);
|
||||||
self.global_space.unmap_output(output);
|
self.global_space.unmap_output(output);
|
||||||
// FIXME: reposition outputs so they are adjacent.
|
self.reposition_outputs(None);
|
||||||
|
|
||||||
let state = self.output_state.remove(output).unwrap();
|
let state = self.output_state.remove(output).unwrap();
|
||||||
self.output_by_name.remove(&output.name()).unwrap();
|
self.output_by_name.remove(&output.name()).unwrap();
|
||||||
@@ -996,11 +1272,14 @@ impl Niri {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the output size changed with an open screenshot UI, close the screenshot UI.
|
// If the output size changed with an open screenshot UI, close the screenshot UI.
|
||||||
if let Some(old_size) = self.screenshot_ui.output_size(&output) {
|
if let Some((old_size, old_scale)) = self.screenshot_ui.output_size(&output) {
|
||||||
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);
|
||||||
if old_size != size {
|
let scale = output.current_scale().integer_scale();
|
||||||
|
// FIXME: scale changes shouldn't matter but they currently do since I haven't quite
|
||||||
|
// figured out how to draw the screenshot textures in physical coordinates.
|
||||||
|
if old_size != size || old_scale != scale {
|
||||||
self.screenshot_ui.close();
|
self.screenshot_ui.close();
|
||||||
self.cursor_manager
|
self.cursor_manager
|
||||||
.set_cursor_image(CursorImageStatus::default_named());
|
.set_cursor_image(CursorImageStatus::default_named());
|
||||||
@@ -1055,6 +1334,19 @@ impl Niri {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (output, pos_within_output) = self.output_under(pos)?;
|
let (output, pos_within_output) = self.output_under(pos)?;
|
||||||
|
|
||||||
|
// Check if some layer-shell surface is on top.
|
||||||
|
let layers = layer_map_for_output(output);
|
||||||
|
let layer_under = |layer| layers.layer_under(layer, pos_within_output).is_some();
|
||||||
|
if layer_under(Layer::Overlay) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mon = self.layout.monitor_for_output(output).unwrap();
|
||||||
|
if !mon.render_above_top_layer() && layer_under(Layer::Top) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let (window, _loc) = self.layout.window_under(output, pos_within_output)?;
|
let (window, _loc) = self.layout.window_under(output, pos_within_output)?;
|
||||||
Some(window)
|
Some(window)
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1549,7 @@ impl Niri {
|
|||||||
layout_output.or_else(layer_shell_output)
|
layout_output.or_else(layer_shell_output)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lock_surface_focus(&self) -> Option<WlSurface> {
|
pub fn lock_surface_focus(&self) -> Option<WlSurface> {
|
||||||
let output_under_cursor = self.output_under_cursor();
|
let output_under_cursor = self.output_under_cursor();
|
||||||
let output = output_under_cursor
|
let output = output_under_cursor
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1268,6 +1560,14 @@ impl Niri {
|
|||||||
state.lock_surface.as_ref().map(|s| s.wl_surface()).cloned()
|
state.lock_surface.as_ref().map(|s| s.wl_surface()).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn refresh_popup_grab(&mut self) {
|
||||||
|
if let Some(grab) = &self.popup_grab {
|
||||||
|
if grab.grab.has_ended() {
|
||||||
|
self.popup_grab = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Schedules an immediate redraw on all outputs if one is not already scheduled.
|
/// Schedules an immediate redraw on all outputs if one is not already scheduled.
|
||||||
pub fn queue_redraw_all(&mut self) {
|
pub fn queue_redraw_all(&mut self) {
|
||||||
let outputs: Vec<_> = self.output_state.keys().cloned().collect();
|
let outputs: Vec<_> = self.output_state.keys().cloned().collect();
|
||||||
@@ -1365,7 +1665,9 @@ impl Niri {
|
|||||||
idx,
|
idx,
|
||||||
);
|
);
|
||||||
|
|
||||||
let pointer_elements = vec![OutputRenderElements::NamedPointer(
|
let mut pointer_elements = vec![];
|
||||||
|
if let Some(texture) = texture {
|
||||||
|
pointer_elements.push(OutputRenderElements::NamedPointer(
|
||||||
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||||
pointer_pos.to_f64(),
|
pointer_pos.to_f64(),
|
||||||
&texture,
|
&texture,
|
||||||
@@ -1374,7 +1676,8 @@ impl Niri {
|
|||||||
None,
|
None,
|
||||||
Kind::Cursor,
|
Kind::Cursor,
|
||||||
)),
|
)),
|
||||||
)];
|
));
|
||||||
|
}
|
||||||
|
|
||||||
(pointer_elements, pointer_pos)
|
(pointer_elements, pointer_pos)
|
||||||
}
|
}
|
||||||
@@ -1522,6 +1825,18 @@ impl Niri {
|
|||||||
elements = self.pointer_element(renderer, output);
|
elements = self.pointer_element(renderer, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next, the exit confirm dialog.
|
||||||
|
if let Some(dialog) = &self.exit_confirm_dialog {
|
||||||
|
if let Some(element) = dialog.render(renderer, output) {
|
||||||
|
elements.push(element.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, the config error notification too.
|
||||||
|
if let Some(element) = self.config_error_notification.render(renderer, output) {
|
||||||
|
elements.push(element.into());
|
||||||
|
}
|
||||||
|
|
||||||
// If the session is locked, draw the lock surface.
|
// If the session is locked, draw the lock surface.
|
||||||
if self.is_locked() {
|
if self.is_locked() {
|
||||||
let state = self.output_state.get(output).unwrap();
|
let state = self.output_state.get(output).unwrap();
|
||||||
@@ -1577,6 +1892,11 @@ impl Niri {
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw the hotkey overlay on top.
|
||||||
|
if let Some(element) = self.hotkey_overlay.render(renderer, output) {
|
||||||
|
elements.push(element.into());
|
||||||
|
}
|
||||||
|
|
||||||
// Get monitor elements.
|
// Get monitor elements.
|
||||||
let mon = self.layout.monitor_for_output(output).unwrap();
|
let mon = self.layout.monitor_for_output(output).unwrap();
|
||||||
let monitor_elements = mon.render_elements(renderer);
|
let monitor_elements = mon.render_elements(renderer);
|
||||||
@@ -1649,6 +1969,11 @@ impl Niri {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.are_animations_ongoing();
|
.are_animations_ongoing();
|
||||||
|
|
||||||
|
self.config_error_notification
|
||||||
|
.advance_animations(target_presentation_time);
|
||||||
|
state.unfinished_animations_remain |=
|
||||||
|
self.config_error_notification.are_animations_ongoing();
|
||||||
|
|
||||||
// Also keep redrawing if the current cursor is animated.
|
// Also keep redrawing if the current cursor is animated.
|
||||||
state.unfinished_animations_remain |= self
|
state.unfinished_animations_remain |= self
|
||||||
.cursor_manager
|
.cursor_manager
|
||||||
@@ -2440,9 +2765,11 @@ impl Niri {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ClientState {
|
pub struct ClientState {
|
||||||
pub compositor_state: CompositorClientState,
|
pub compositor_state: CompositorClientState,
|
||||||
|
pub can_view_decoration_globals: bool,
|
||||||
|
/// Whether this client is denied from the restricted protocols such as security-context.
|
||||||
|
pub restricted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientData for ClientState {
|
impl ClientData for ClientState {
|
||||||
@@ -2560,6 +2887,7 @@ pub enum OutputRenderElements<R: NiriRenderer> {
|
|||||||
NamedPointer(PrimaryGpuTextureRenderElement),
|
NamedPointer(PrimaryGpuTextureRenderElement),
|
||||||
SolidColor(SolidColorRenderElement),
|
SolidColor(SolidColorRenderElement),
|
||||||
ScreenshotUi(ScreenshotUiRenderElement),
|
ScreenshotUi(ScreenshotUiRenderElement),
|
||||||
|
ConfigErrorNotification(ConfigErrorNotificationRenderElement<R>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
||||||
@@ -2570,6 +2898,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.id(),
|
Self::NamedPointer(elem) => elem.id(),
|
||||||
Self::SolidColor(elem) => elem.id(),
|
Self::SolidColor(elem) => elem.id(),
|
||||||
Self::ScreenshotUi(elem) => elem.id(),
|
Self::ScreenshotUi(elem) => elem.id(),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.id(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2580,6 +2909,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.current_commit(),
|
Self::NamedPointer(elem) => elem.current_commit(),
|
||||||
Self::SolidColor(elem) => elem.current_commit(),
|
Self::SolidColor(elem) => elem.current_commit(),
|
||||||
Self::ScreenshotUi(elem) => elem.current_commit(),
|
Self::ScreenshotUi(elem) => elem.current_commit(),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.current_commit(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2590,6 +2920,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.geometry(scale),
|
Self::NamedPointer(elem) => elem.geometry(scale),
|
||||||
Self::SolidColor(elem) => elem.geometry(scale),
|
Self::SolidColor(elem) => elem.geometry(scale),
|
||||||
Self::ScreenshotUi(elem) => elem.geometry(scale),
|
Self::ScreenshotUi(elem) => elem.geometry(scale),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.geometry(scale),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2600,6 +2931,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.transform(),
|
Self::NamedPointer(elem) => elem.transform(),
|
||||||
Self::SolidColor(elem) => elem.transform(),
|
Self::SolidColor(elem) => elem.transform(),
|
||||||
Self::ScreenshotUi(elem) => elem.transform(),
|
Self::ScreenshotUi(elem) => elem.transform(),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.transform(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2610,6 +2942,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.src(),
|
Self::NamedPointer(elem) => elem.src(),
|
||||||
Self::SolidColor(elem) => elem.src(),
|
Self::SolidColor(elem) => elem.src(),
|
||||||
Self::ScreenshotUi(elem) => elem.src(),
|
Self::ScreenshotUi(elem) => elem.src(),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.src(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2624,6 +2957,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.damage_since(scale, commit),
|
Self::NamedPointer(elem) => elem.damage_since(scale, commit),
|
||||||
Self::SolidColor(elem) => elem.damage_since(scale, commit),
|
Self::SolidColor(elem) => elem.damage_since(scale, commit),
|
||||||
Self::ScreenshotUi(elem) => elem.damage_since(scale, commit),
|
Self::ScreenshotUi(elem) => elem.damage_since(scale, commit),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.damage_since(scale, commit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2634,6 +2968,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.opaque_regions(scale),
|
Self::NamedPointer(elem) => elem.opaque_regions(scale),
|
||||||
Self::SolidColor(elem) => elem.opaque_regions(scale),
|
Self::SolidColor(elem) => elem.opaque_regions(scale),
|
||||||
Self::ScreenshotUi(elem) => elem.opaque_regions(scale),
|
Self::ScreenshotUi(elem) => elem.opaque_regions(scale),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.opaque_regions(scale),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2644,6 +2979,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.alpha(),
|
Self::NamedPointer(elem) => elem.alpha(),
|
||||||
Self::SolidColor(elem) => elem.alpha(),
|
Self::SolidColor(elem) => elem.alpha(),
|
||||||
Self::ScreenshotUi(elem) => elem.alpha(),
|
Self::ScreenshotUi(elem) => elem.alpha(),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.alpha(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2654,6 +2990,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
|
|||||||
Self::NamedPointer(elem) => elem.kind(),
|
Self::NamedPointer(elem) => elem.kind(),
|
||||||
Self::SolidColor(elem) => elem.kind(),
|
Self::SolidColor(elem) => elem.kind(),
|
||||||
Self::ScreenshotUi(elem) => elem.kind(),
|
Self::ScreenshotUi(elem) => elem.kind(),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.kind(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2678,6 +3015,7 @@ impl RenderElement<GlesRenderer> for OutputRenderElements<GlesRenderer> {
|
|||||||
Self::ScreenshotUi(elem) => {
|
Self::ScreenshotUi(elem) => {
|
||||||
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
||||||
}
|
}
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.draw(frame, src, dst, damage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2688,6 +3026,7 @@ impl RenderElement<GlesRenderer> for OutputRenderElements<GlesRenderer> {
|
|||||||
Self::NamedPointer(elem) => elem.underlying_storage(renderer),
|
Self::NamedPointer(elem) => elem.underlying_storage(renderer),
|
||||||
Self::SolidColor(elem) => elem.underlying_storage(renderer),
|
Self::SolidColor(elem) => elem.underlying_storage(renderer),
|
||||||
Self::ScreenshotUi(elem) => elem.underlying_storage(renderer),
|
Self::ScreenshotUi(elem) => elem.underlying_storage(renderer),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.underlying_storage(renderer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2714,6 +3053,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
|
|||||||
Self::ScreenshotUi(elem) => {
|
Self::ScreenshotUi(elem) => {
|
||||||
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
||||||
}
|
}
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.draw(frame, src, dst, damage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2727,6 +3067,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
|
|||||||
Self::NamedPointer(elem) => elem.underlying_storage(renderer),
|
Self::NamedPointer(elem) => elem.underlying_storage(renderer),
|
||||||
Self::SolidColor(elem) => elem.underlying_storage(renderer),
|
Self::SolidColor(elem) => elem.underlying_storage(renderer),
|
||||||
Self::ScreenshotUi(elem) => elem.underlying_storage(renderer),
|
Self::ScreenshotUi(elem) => elem.underlying_storage(renderer),
|
||||||
|
Self::ConfigErrorNotification(elem) => elem.underlying_storage(renderer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2760,3 +3101,9 @@ impl<R: NiriRenderer> From<ScreenshotUiRenderElement> for OutputRenderElements<R
|
|||||||
Self::ScreenshotUi(x)
|
Self::ScreenshotUi(x)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R: NiriRenderer> From<ConfigErrorNotificationRenderElement<R>> for OutputRenderElements<R> {
|
||||||
|
fn from(x: ConfigErrorNotificationRenderElement<R>) -> Self {
|
||||||
|
Self::ConfigErrorNotification(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use smithay::backend::renderer::element::texture::TextureRenderElement;
|
|||||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||||
use smithay::backend::renderer::utils::CommitCounter;
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
use smithay::backend::renderer::{Bind, ExportMem, ImportAll, Offscreen, Renderer, Texture};
|
use smithay::backend::renderer::{
|
||||||
|
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
||||||
|
};
|
||||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
@@ -11,6 +13,7 @@ use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
|||||||
/// Trait with our main renderer requirements to save on the typing.
|
/// Trait with our main renderer requirements to save on the typing.
|
||||||
pub trait NiriRenderer:
|
pub trait NiriRenderer:
|
||||||
ImportAll
|
ImportAll
|
||||||
|
+ ImportMem
|
||||||
+ ExportMem
|
+ ExportMem
|
||||||
+ Bind<Dmabuf>
|
+ Bind<Dmabuf>
|
||||||
+ Offscreen<GlesTexture>
|
+ Offscreen<GlesTexture>
|
||||||
@@ -28,7 +31,7 @@ pub trait NiriRenderer:
|
|||||||
|
|
||||||
impl<R> NiriRenderer for R
|
impl<R> NiriRenderer for R
|
||||||
where
|
where
|
||||||
R: ImportAll + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||||
R::TextureId: Texture + Clone + 'static,
|
R::TextureId: Texture + Clone + 'static,
|
||||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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};
|
||||||
@@ -18,7 +19,6 @@ use smithay::output::{Output, WeakOutput};
|
|||||||
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
|
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||||
|
|
||||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
use crate::config::Action;
|
|
||||||
use crate::render_helpers::PrimaryGpuTextureRenderElement;
|
use crate::render_helpers::PrimaryGpuTextureRenderElement;
|
||||||
|
|
||||||
const BORDER: i32 = 2;
|
const BORDER: i32 = 2;
|
||||||
@@ -41,6 +41,7 @@ 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],
|
||||||
@@ -106,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,
|
||||||
);
|
);
|
||||||
@@ -126,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,
|
||||||
@@ -330,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
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -11,12 +11,11 @@ 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::output::Output;
|
||||||
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use crate::config::Config;
|
|
||||||
|
|
||||||
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
|
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
|
||||||
(t.0.clone(), t.1.clone())
|
(t.0.clone(), t.1.clone())
|
||||||
}
|
}
|
||||||
@@ -191,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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user