Compare commits

...

74 Commits

Author SHA1 Message Date
Ivan Molodetskikh 6945ccde18 Bump version to 0.1.0-beta.1 2024-01-20 09:38:42 +04:00
Ivan Molodetskikh e86e9c6c9a CI: Add a Fedora build 2024-01-20 09:25:50 +04:00
Ivan Molodetskikh dc47de178f Add an option to skip the hotkey overlay at startup 2024-01-20 08:31:05 +04:00
Ivan Molodetskikh 65e864965e Print git version in clap too 2024-01-19 20:46:10 +04:00
Ivan Molodetskikh 55ad36addc layout: Fix crash due to workspace transfer during switch 2024-01-19 20:24:59 +04:00
Ivan Molodetskikh 26c8cbb961 layout: Fix crash due to workspace cleanup during switch 2024-01-19 20:24:18 +04:00
Ivan Molodetskikh 031133c052 README: Add link to important software wiki page 2024-01-19 07:01:56 -08:00
Ivan Molodetskikh a6f821d3fa Update dependencies 2024-01-19 09:41:16 +04:00
Ivan Molodetskikh 475b3df2b5 Don't crash when failing to render a cursor
I only hit this when the renderer was completely busted, but
nevertheless.
2024-01-19 09:13:32 +04:00
Ivan Molodetskikh 1541835f00 Prettify Return => Enter key 2024-01-19 08:35:36 +04:00
Ivan Molodetskikh 4b9cb2f0d3 Add exit confirmation dialog 2024-01-19 08:33:54 +04:00
Ivan Molodetskikh 3461c66d2c Redraw upon starting PW stream
Otherwise it may take a while for the first frame to arrive.
2024-01-18 21:16:36 +04:00
Ivan Molodetskikh 011c91c98a Add an important hotkeys overlay 2024-01-18 20:32:44 +04:00
Ivan Molodetskikh edafa139f6 portal: Name and sort monitors, fix session restore
xdp-gnome restores by a combination of model + make + serial. We
currently can't set those reliably (until libdisplay-info most monitors
will have them unknown) so pass the connector name instead. This will
work as expected in most cases.
2024-01-18 16:31:04 +04:00
Ivan Molodetskikh fa9b3ed106 Add a config parse error notification
We can't rely on a notification daemon being available, especially
during initial niri setup. So, render our own.
2024-01-18 12:44:05 +04:00
Ivan Molodetskikh cc62a403c0 Update Smithay (deadlock fix) 2024-01-18 11:14:39 +04:00
Ivan Molodetskikh 0f85c79548 Watch config path even if it didn't exist at startup 2024-01-18 11:13:36 +04:00
Ivan Molodetskikh 6beef26662 Fix dependency sorting 2024-01-18 11:00:49 +04:00
Ivan Molodetskikh 616055e205 Update README.md 2024-01-17 03:15:05 -08:00
Ivan Molodetskikh 40c85da102 Add an IPC socket and a niri msg outputs subcommand 2024-01-17 10:45:18 +04:00
Ivan Molodetskikh 768b326028 Rename connectors to enabled_outputs 2024-01-17 10:25:23 +04:00
Ivan Molodetskikh f068157f55 Add a calloop futures executor 2024-01-17 10:24:01 +04:00
Ivan Molodetskikh 6703d5ce72 tty: Add Tracy span to on_output_config_changed() 2024-01-17 10:21:40 +04:00
Ivan Molodetskikh 12590f689a Write a comment on xdg-decoration lack of live-reload 2024-01-16 20:43:28 +04:00
Ivan Molodetskikh 4656332d07 Add live-reload to libinput settings 2024-01-16 20:29:37 +04:00
Ivan Molodetskikh 954f711bf3 Extract apply_libinput_settings() 2024-01-16 20:28:37 +04:00
Ivan Molodetskikh c09c964420 default-config: Add example for spawn with bash 2024-01-16 20:08:31 +04:00
Ivan Molodetskikh 1f9abaaa58 Add live-reload for output mode 2024-01-16 18:02:30 +04:00
Ivan Molodetskikh eb4946c3d8 tty: Extract pick_mode() 2024-01-16 18:01:25 +04:00
Ivan Molodetskikh 5f440f7be3 Add live-reload for output on/off 2024-01-16 15:34:00 +04:00
Ivan Molodetskikh 6644cc16ff tty: Remove connector arg from connector_disconnected() 2024-01-16 15:33:37 +04:00
Ivan Molodetskikh 9e667efc4c Close layer surfaces upon output removal
Fixes https://github.com/YaLTeR/niri/issues/23
2024-01-16 13:28:29 +04:00
Ivan Molodetskikh 8a7e4bc3cd Add Tracy span to Config::load and parse 2024-01-16 12:53:40 +04:00
Ivan Molodetskikh 69907f123d Add live-reload of output scales 2024-01-16 11:34:34 +04:00
Ivan Molodetskikh 6ca3b6ddb5 Move output scale setting into niri 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh fc5a080ca5 layout: Fix surface leaving output when consuming into column 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh 83719a49b7 Add live-reload of output positions 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh da4967d43c Reposition all outputs on any change
This way the positioning is independent of the order of plugging in.
2024-01-16 08:43:28 +04:00
Ivan Molodetskikh d958a9679c Change message from debug to trace 2024-01-16 07:38:52 +04:00
Ivan Molodetskikh e4643c6dbe Implement security-context, hide some protocols from it 2024-01-15 16:02:07 +04:00
Ivan Molodetskikh 59763fd0da Hide decoration globals when we need CSD
This gets the current SDL2 with libdecor working.
2024-01-15 16:01:01 +04:00
Ivan Molodetskikh 533659eef8 Update Smithay 2024-01-15 15:59:36 +04:00
Ivan Molodetskikh 81443d8e16 Change default binds to move columns instead of windows 2024-01-15 11:51:04 +04:00
Ivan Molodetskikh fb38ae26c9 Add move-column-to-monitor* binds
As opposed to move-window-to-monitor*
2024-01-15 10:36:59 +04:00
Ivan Molodetskikh cc4acdf24a Add move-column-to-workspace* binds
As opposed to move-window-to-workspace*
2024-01-15 10:31:44 +04:00
Ivan Molodetskikh 2506d43bb9 xdg-decoration: Document SDL2 bug 2024-01-14 09:28:03 +04:00
Ivan Molodetskikh d899bc4712 Revert "Be more insistent on CSD by default"
This reverts commit 43e2cf14d2.

SDL2 until very recently (unreleased version) has had a bug where
changing the decoration mode to client-side during its initial window
creation would keep the window permanently hidden. Breaking all SDL2
apps for years to come is unfortunately not a good solution.
2024-01-14 09:23:15 +04:00
Ivan Molodetskikh 14552d856c xdg-decoration: Always send configure
The protocol wording seems to require it.
2024-01-14 08:57:46 +04:00
Ivan Molodetskikh 632a00fcca Implement popup grabs 2024-01-13 09:00:57 +04:00
Ivan Molodetskikh 80652a0765 Remove is_grabbed check for changing active window
When clicking outside of the popup grab, the click does go through if
the popup is dismissed. This makes the active window change go through
too.
2024-01-13 08:17:53 +04:00
Ivan Molodetskikh a52bf92ae1 Add missing screen redraws on focus changes
The window isn't guaranteed to commit a buffer.
2024-01-13 08:17:53 +04:00
Ivan Molodetskikh 952ff02982 Keep track of keyboard focus manually 2024-01-12 17:14:18 +04:00
Ivan Molodetskikh e1adabed2d Rename update_focus -> update_keyboard_focus 2024-01-12 16:53:00 +04:00
Ivan Molodetskikh b5c4f9ed2a Remove obsolete FIXME comment
It's implemented now.
2024-01-12 14:54:59 +04:00
Ivan Molodetskikh d39f1897c7 Force redraws on window activation
Activating a window does not necessarily make it commit a buffer and
update the screen for us.
2024-01-12 08:48:22 +04:00
Ivan Molodetskikh e46b614c2b Fix clicks activating windows through layer-shell surfaces 2024-01-12 08:45:39 +04:00
Ivan Molodetskikh 78aa08b100 Silence the two type complexity lints
meh
2024-01-11 22:10:12 +04:00
Ivan Molodetskikh d8626fcab0 Fix clippy suggestion 2024-01-11 21:42:00 +04:00
Ivan Molodetskikh f4e04ac910 Mark cause_panic() as #[inline(never)]
Despite compiling with frame pointers, inlining cause_panic() makes the
backtrace omit its frame and even the source location in main...
2024-01-11 18:30:54 +04:00
Bill Sun 236abd9d9d Add Nix Flake (#77)
* Add Nix Flake

Co-authored-by: Bryce Berger <bryce.z.berger@gmail.com>

* Describe nix flake in readme

* Add `niri-config` to build source list

* Add maintainer info

Add comment at top to indicate the Nix Flake file
is community maintained.

* Clarify Nix/NixOS README instructions

* Shorten Nix/NixOS build instructions

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>

* Move NixOS installation instruction to "Tip" section

---------

Co-authored-by: Bryce Berger <bryce.z.berger@gmail.com>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-01-10 22:43:46 -08:00
Ivan Molodetskikh b2df3e104f Document debug settings in the default config 2024-01-09 08:18:34 +04:00
Ivan Molodetskikh ec2d339a86 Add panic subcommand to check backtraces 2024-01-09 08:08:38 +04:00
Ivan Molodetskikh 629a2ccb47 layout: Improve Options randomization in tests 2024-01-08 20:57:53 +04:00
Thomas Versteeg fb93038bd8 Add center-focused-column setting 2024-01-08 17:37:18 +04:00
Ivan Molodetskikh 71fef2ad2e Add a few mouse libinput settings 2024-01-08 11:53:34 +04:00
Ivan Molodetskikh c6841f19e9 Add touchpad tap-button-map setting 2024-01-08 10:32:04 +04:00
Ivan Molodetskikh e1971c4af5 Add touchpad dwt setting 2024-01-08 10:24:00 +04:00
Ivan Molodetskikh 07b1d0e98d Add touchpad accel-profile setting 2024-01-08 10:23:53 +04:00
Ivan Molodetskikh ffe25f5cc4 Update Smithay and dependencies 2024-01-07 17:44:08 +04:00
Ivan Molodetskikh 43e2cf14d2 Be more insistent on CSD by default
By default I want CSD so that people don't necessarily have to learn all
of the shortcuts right away, and can at least use the X to close
windows.
2024-01-07 09:41:56 +04:00
Ivan Molodetskikh 2c59131f7f Remove debuginfo from niri-config
Finally, the file sizes are back to sane.
2024-01-07 09:28:50 +04:00
Ivan Molodetskikh 64c41fa2c8 Move config into a separate crate
Get miette and knuffel deps contained within.
2024-01-07 09:28:14 +04:00
Ivan Molodetskikh 4e0aa39113 [cfg-breaking] Move layout settings into their own scope 2024-01-06 13:04:21 +04:00
Ivan Molodetskikh dcb80efc88 Bump version to 0.1.0-alpha.3 2024-01-06 09:30:46 +04:00
40 changed files with 4148 additions and 784 deletions
+19 -2
View File
@@ -35,7 +35,7 @@ jobs:
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
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
run: |
@@ -78,7 +78,7 @@ jobs:
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
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
run: |
@@ -107,3 +107,20 @@ jobs:
- name: Run rustfmt
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
View File
@@ -1 +1,2 @@
/target
/result
Generated
+376 -133
View File
File diff suppressed because it is too large Load Diff
+49 -22
View File
@@ -1,13 +1,37 @@
[package]
name = "niri"
version = "0.1.0-alpha.2"
[workspace.package]
version = "0.1.0-beta.1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[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"
repository = "https://github.com/YaLTeR/niri"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
@@ -15,34 +39,37 @@ anyhow = { version = "1.0.79" }
arrayvec = "0.7.4"
async-channel = { version = "2.1.1", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.1"
clap = { version = "4.4.13", features = ["derive"] }
bitflags = "2.4.2"
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
clap = { version = "4.4.18", features = ["derive", "string"] }
directories = "5.0.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
keyframe = { version = "1.1.1", default-features = false }
knuffel = "3.2.0"
libc = "0.2.151"
logind-zbus = { version = "3.1.2", optional = true }
libc = "0.2.152"
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 }
pangocairo = "0.18.0"
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"] }
profiling = "1.0.13"
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 = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracy-client = { version = "0.16.5", default-features = false }
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
zbus = { version = "3.14.1", optional = true }
[dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
default-features = false
workspace = true
features = [
"backend_drm",
"backend_egl",
@@ -58,10 +85,6 @@ features = [
"wayland_frontend",
]
[dependencies.smithay-drm-extras]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
@@ -80,8 +103,12 @@ debug = "line-tables-only"
overflow-checks = true
lto = "thin"
[profile.release.package.niri-config]
# knuffel with chomsky generates a metric ton of debuginfo.
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~alpha.2"
version = "0.1.0~beta.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+68 -26
View File
@@ -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.
![](https://github.com/YaLTeR/niri/assets/1794388/16f87a4a-afac-49aa-b3e6-5e6f16c943a9)
![](https://github.com/YaLTeR/niri/assets/1794388/e35fd9e1-105b-4bd5-94c9-207fd6fb3c18)
## 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
@@ -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
## Idea
## Inspiration
Niri implements scrollable tiling, heavily inspired by [PaperWM].
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.
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
With multiple monitors, every monitor has its own separate window strip.
Windows can never "overflow" onto an adjacent monitor.
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.
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
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.
@@ -36,7 +52,7 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
> [!TIP]
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
>
> NixOS users, check out https://github.com/sodiboo/niri-flake
> For NixOS users, check out https://github.com/sodiboo/niri-flake
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 add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
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:
```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`.
### 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
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.
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
[This wiki page](https://github.com/YaLTeR/niri/wiki/Important-Software) explains how to run important software required for normal desktop use, including portals.
### Xwayland
See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri.
### IPC
You can communicate with the running niri instance over an IPC socket.
Check `niri msg --help` for available commands.
The `--json` flag prints the response in JSON, rather than formatted.
For example, `niri msg --json outputs`.
For programmatic access, check the [niri-ipc sub-crate](./niri-ipc/) which defines the types.
The communication over the IPC socket happens in JSON.
## Default Hotkeys
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 |
| ------ | ----------- |
| <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>D</kbd> | Spawn `fuzzel` (application launcher) |
| <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>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>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>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>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>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 column to the workspace above |
| <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>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 |
@@ -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).
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.
Though, some settings are still missing live-reload support.
Notably, output modes and positions will only apply when the output is reconnected.
Niri will live-reload most of the configuration settings, like key binds or gaps or output modes, as you change the config file.
## 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
[mako]: https://github.com/emersion/mako
Generated
+138
View File
@@ -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
}
+106
View File
@@ -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"
];
};
}
);
}
+16
View File
@@ -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
+252 -105
View File
@@ -1,12 +1,15 @@
use std::path::PathBuf;
#[macro_use]
extern crate tracing;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use bitflags::bitflags;
use directories::ProjectDirs;
use miette::{miette, Context, IntoDiagnostic};
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::{Keysym, XkbConfig};
use smithay::reexports::input;
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
@@ -17,21 +20,11 @@ pub struct Config {
#[knuffel(children(name = "spawn-at-startup"))]
pub spawn_at_startup: Vec<SpawnAtStartup>,
#[knuffel(child, default)]
pub focus_ring: FocusRing,
#[knuffel(child, default = default_border())]
pub border: FocusRing,
pub layout: Layout,
#[knuffel(child, default)]
pub prefer_no_csd: bool,
#[knuffel(child, default)]
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(
child,
unwrap(argument),
@@ -41,6 +34,8 @@ pub struct Config {
]
pub screenshot_path: Option<String>,
#[knuffel(child, default)]
pub hotkey_overlay: HotkeyOverlay,
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
pub debug: DebugConfig,
@@ -54,6 +49,8 @@ pub struct Input {
#[knuffel(child, default)]
pub touchpad: Touchpad,
#[knuffel(child, default)]
pub mouse: Mouse,
#[knuffel(child, default)]
pub tablet: Tablet,
#[knuffel(child)]
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)]
pub enum TrackLayout {
/// The layout change is global.
@@ -113,9 +122,55 @@ pub struct Touchpad {
#[knuffel(child)]
pub tap: bool,
#[knuffel(child)]
pub dwt: bool,
#[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>,
#[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)]
@@ -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 {
#[knuffel(property)]
pub x: i32,
@@ -158,13 +213,31 @@ pub struct Position {
pub y: i32,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mode {
pub width: u16,
pub height: u16,
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)]
pub struct SpawnAtStartup {
#[knuffel(arguments)]
@@ -265,6 +338,12 @@ pub struct Struts {
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)]
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
@@ -336,6 +415,9 @@ pub enum Action {
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[knuffel(argument)] u8),
MoveWorkspaceDown,
MoveWorkspaceUp,
FocusMonitorLeft,
@@ -346,11 +428,16 @@ pub enum Action {
MoveWindowToMonitorRight,
MoveWindowToMonitorDown,
MoveWindowToMonitorUp,
MoveColumnToMonitorLeft,
MoveColumnToMonitorRight,
MoveColumnToMonitorDown,
MoveColumnToMonitorUp,
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
SwitchPresetColumnWidth,
MaximizeColumn,
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
SwitchLayout(#[knuffel(argument)] LayoutAction),
ShowHotkeyOverlay,
}
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -400,28 +487,23 @@ impl Default for DebugConfig {
}
impl Config {
pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> {
let path = if let Some(path) = path {
path
} else {
let mut path = ProjectDirs::from("", "", "niri")
.ok_or_else(|| miette!("error retrieving home directory"))?
.config_dir()
.to_owned();
path.push("config.kdl");
path
};
pub fn load(path: &Path) -> miette::Result<Self> {
let _span = tracy_client::span!("Config::load");
Self::load_internal(path).context("error loading config")
}
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()
.with_context(|| format!("error reading {path:?}"))?;
let config = Self::parse("config.kdl", &contents).context("error parsing")?;
debug!("loaded config from {path:?}");
Ok((config, path))
Ok(config)
}
pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
let _span = tracy_client::span!("Config::parse");
knuffel::parse(filename, text)
}
}
@@ -430,7 +512,7 @@ impl Default for Config {
fn default() -> Self {
Config::parse(
"default-config.kdl",
include_str!("../resources/default-config.kdl"),
include_str!("../../resources/default-config.kdl"),
)
.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)]
mod tests {
use miette::NarratableReportHandler;
@@ -591,7 +705,16 @@ mod tests {
touchpad {
tap
dwt
accel-speed 0.2
accel-profile "flat"
tap-button-map "left-middle-right"
}
mouse {
natural-scroll
accel-speed 0.4
accel-profile "flat"
}
tablet {
@@ -607,20 +730,41 @@ mod tests {
mode "1920x1080@144"
}
layout {
focus-ring {
width 5
active-color 0 100 200 255
inactive-color 255 200 100 0
}
border {
width 3
active-color 0 100 200 255
inactive-color 255 200 100 0
}
preset-column-widths {
proportion 0.25
proportion 0.5
fixed 960
fixed 1280
}
default-column-width { proportion 0.25; }
gaps 8
struts {
left 1
right 2
top 3
}
center-focused-column "on-overflow"
}
spawn-at-startup "alacritty" "-e" "fish"
focus-ring {
width 5
active-color 0 100 200 255
inactive-color 255 200 100 0
}
border {
width 3
active-color 0 100 200 255
inactive-color 255 200 100 0
}
prefer-no-csd
cursor {
@@ -628,25 +772,12 @@ mod tests {
xcursor-size 16
}
preset-column-widths {
proportion 0.25
proportion 0.5
fixed 960
fixed 1280
}
default-column-width { proportion 0.25; }
gaps 8
struts {
left 1
right 2
top 3
}
screenshot-path "~/Screenshots/screenshot.png"
hotkey-overlay {
skip-at-startup
}
binds {
Mod+T { spawn "alacritty"; }
Mod+Q { close-window; }
@@ -675,8 +806,16 @@ mod tests {
},
touchpad: Touchpad {
tap: true,
dwt: true,
natural_scroll: false,
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 {
map_to_output: Some("eDP-1".to_owned()),
@@ -694,61 +833,69 @@ mod tests {
refresh: Some(144.),
}),
}],
layout: Layout {
focus_ring: FocusRing {
off: false,
width: 5,
active_color: Color {
r: 0,
g: 100,
b: 200,
a: 255,
},
inactive_color: Color {
r: 255,
g: 200,
b: 100,
a: 0,
},
},
border: FocusRing {
off: false,
width: 3,
active_color: Color {
r: 0,
g: 100,
b: 200,
a: 255,
},
inactive_color: Color {
r: 255,
g: 200,
b: 100,
a: 0,
},
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
PresetWidth::Proportion(0.5),
PresetWidth::Fixed(960),
PresetWidth::Fixed(1280),
],
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(
0.25,
)])),
gaps: 8,
struts: Struts {
left: 1,
right: 2,
top: 3,
bottom: 0,
},
center_focused_column: CenterFocusedColumn::OnOverflow,
},
spawn_at_startup: vec![SpawnAtStartup {
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
}],
focus_ring: FocusRing {
off: false,
width: 5,
active_color: Color {
r: 0,
g: 100,
b: 200,
a: 255,
},
inactive_color: Color {
r: 255,
g: 200,
b: 100,
a: 0,
},
},
border: FocusRing {
off: false,
width: 3,
active_color: Color {
r: 0,
g: 100,
b: 200,
a: 255,
},
inactive_color: Color {
r: 255,
g: 200,
b: 100,
a: 0,
},
},
prefer_no_csd: true,
cursor: Cursor {
xcursor_theme: String::from("breeze_cursors"),
xcursor_size: 16,
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
PresetWidth::Proportion(0.5),
PresetWidth::Fixed(960),
PresetWidth::Fixed(1280),
],
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(0.25)])),
gaps: 8,
struts: Struts {
left: 1,
right: 2,
top: 3,
bottom: 0,
},
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
hotkey_overlay: HotkeyOverlay {
skip_at_startup: true,
},
binds: Binds(vec![
Bind {
key: Key {
+11
View File
@@ -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
+55
View File
@@ -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,
}
+169 -98
View File
@@ -27,8 +27,17 @@ input {
// Omitting settings disables them, or leaves them at their default values.
touchpad {
tap
// dwt
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// tap-button-map "left-middle-right"
}
mouse {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
tablet {
@@ -45,7 +54,8 @@ input {
// 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".
// Remember to uncommend the node by removing "/-"!
/-output "eDP-1" {
@@ -60,7 +70,7 @@ input {
// If the refresh rate is omitted, niri will pick the highest refresh rate
// for the resolution.
// 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"
// Position of the output in the global coordinate space.
@@ -75,37 +85,80 @@ input {
position x=1280 y=0
}
layout {
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
// off
// How many logical pixels the ring extends out from the windows.
width 4
// Color of the ring on the active monitor: red, green, blue, alpha.
active-color 127 200 255 255
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
}
// You can also add a border. It's similar to the focus ring, but always visible.
border {
// The settings are the same as for the focus ring.
// If you enable the border, you probably want to disable the focus ring.
off
width 4
active-color 255 200 127 255
inactive-color 80 80 80 255
}
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.333
proportion 0.5
proportion 0.667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// Set gaps around windows in logical pixels.
gaps 16
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
// layer-shell panels and regular gaps.
struts {
// left 64
// right 64
// top 64
// bottom 64
}
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
// - "always", the focused column will always be centered.
center-focused-column "never"
}
// Add lines like this to spawn processes at startup.
// 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.
focus-ring {
// Uncomment this line to disable the focus ring.
// off
// How many logical pixels the ring extends out from the windows.
width 4
// Color of the ring on the active monitor: red, green, blue, alpha.
active-color 127 200 255 255
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
}
// You can also add a border. It's similar to the focus ring, but always visible.
border {
// The settings are the same as for the focus ring.
// If you enable the border, you probably want to disable the focus ring.
off
width 4
active-color 255 200 127 255
inactive-color 80 80 80 255
}
cursor {
// Change the theme and size of the cursor as well as set the
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
@@ -118,39 +171,6 @@ cursor {
// 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.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.333
proportion 0.5
proportion 0.667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// Set gaps around windows in logical pixels.
gaps 16
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
// layer-shell panels and regular gaps.
struts {
// left 64
// right 64
// top 64
// bottom 64
}
// You can change the path where screenshots are saved.
// A ~ at the front will be expanded to the home directory.
// The path is formatted with strftime(3) to give you the screenshot date and time.
@@ -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.
// 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 {
// 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
@@ -167,34 +193,41 @@ binds {
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
// 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.
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
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.
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
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+Down { focus-window-down; }
Mod+Up { focus-window-up; }
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+Down { move-window-down; }
Mod+Ctrl+Up { move-window-up; }
Mod+Ctrl+Right { move-column-right; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+J { move-window-down; }
Mod+Ctrl+K { move-window-up; }
Mod+Ctrl+L { move-column-right; }
// Alternative commands that move across workspaces when reaching
// the first or last window in a column.
@@ -208,37 +241,45 @@ binds {
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
Mod+Shift+H { focus-monitor-left; }
Mod+Shift+J { focus-monitor-down; }
Mod+Shift+K { focus-monitor-up; }
Mod+Shift+L { focus-monitor-right; }
Mod+Shift+Left { focus-monitor-left; }
Mod+Shift+Down { focus-monitor-down; }
Mod+Shift+Up { focus-monitor-up; }
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+J { move-window-to-monitor-down; }
Mod+Shift+Ctrl+K { move-window-to-monitor-up; }
Mod+Shift+Ctrl+L { move-window-to-monitor-right; }
Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-window-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-window-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-window-to-monitor-right; }
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
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_Up { focus-workspace-up; }
Mod+Ctrl+U { move-window-to-workspace-down; }
Mod+Ctrl+I { move-window-to-workspace-up; }
Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
Mod+Ctrl+Page_Up { move-window-to-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
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_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
@@ -249,15 +290,18 @@ binds {
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
Mod+Ctrl+1 { move-window-to-workspace 1; }
Mod+Ctrl+2 { move-window-to-workspace 2; }
Mod+Ctrl+3 { move-window-to-workspace 3; }
Mod+Ctrl+4 { move-window-to-workspace 4; }
Mod+Ctrl+5 { move-window-to-workspace 5; }
Mod+Ctrl+6 { move-window-to-workspace 6; }
Mod+Ctrl+7 { move-window-to-workspace 7; }
Mod+Ctrl+8 { move-window-to-workspace 8; }
Mod+Ctrl+9 { move-window-to-workspace 9; }
Mod+Ctrl+1 { move-column-to-workspace 1; }
Mod+Ctrl+2 { move-column-to-workspace 2; }
Mod+Ctrl+3 { move-column-to-workspace 3; }
Mod+Ctrl+4 { move-column-to-workspace 4; }
Mod+Ctrl+5 { move-column-to-workspace 5; }
Mod+Ctrl+6 { move-column-to-workspace 6; }
Mod+Ctrl+7 { move-column-to-workspace 7; }
Mod+Ctrl+8 { move-column-to-workspace 8; }
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+Period { expel-window-from-column; }
@@ -299,3 +343,30 @@ binds {
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"
}
+5
View File
@@ -50,4 +50,9 @@ impl Animation {
pub fn to(&self) -> f64 {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
}
+20 -4
View File
@@ -1,4 +1,6 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -110,11 +112,18 @@ impl Backend {
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
match self {
Backend::Tty(tty) => tty.connectors(),
Backend::Winit(winit) => winit.connectors(),
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
match self {
Backend::Tty(tty) => tty.enabled_outputs(),
Backend::Winit(winit) => winit.enabled_outputs(),
}
}
@@ -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 {
if let Self::Tty(v) = self {
v
+302 -102
View File
@@ -9,6 +9,7 @@ use std::{io, mem};
use anyhow::{anyhow, Context};
use libc::dev_t;
use niri_config::Config;
use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufAllocator};
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
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::udev::{self, UdevBackend, UdevEvent};
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::{Dispatcher, LoopHandle, RegistrationToken};
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::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 super::RenderResult;
use crate::config::Config;
use crate::frame_clock::FrameClock;
use crate::niri::{RedrawState, State};
use crate::render_helpers::AsGlesRenderer;
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 primary GPU.
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<
@@ -220,7 +222,8 @@ impl Tty {
devices: HashMap::new(),
dmabuf_global: 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
.drm_scanner
.crtcs()
.map(|(conn, crtc)| (conn.clone(), crtc))
.map(|(_conn, crtc)| crtc)
.collect();
for (conn, crtc) in crtcs {
self.connector_disconnected(niri, node, conn, crtc);
for crtc in crtcs {
self.connector_disconnected(niri, node, crtc);
}
let device = self.devices.get_mut(&node).unwrap();
@@ -366,6 +369,8 @@ impl Tty {
warn!("error adding device: {err:?}");
}
}
self.refresh_ipc_outputs();
}
}
}
@@ -506,12 +511,13 @@ impl Tty {
}
}
DrmScanEvent::Disconnected {
connector,
crtc: Some(crtc),
} => self.connector_disconnected(niri, node, connector, crtc),
crtc: Some(crtc), ..
} => self.connector_disconnected(niri, node, crtc),
_ => (),
}
}
self.refresh_ipc_outputs();
}
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
@@ -530,11 +536,11 @@ impl Tty {
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(info, crtc)| (info.clone(), crtc))
.map(|(_info, crtc)| crtc)
.collect();
for (connector, crtc) in crtcs {
self.connector_disconnected(niri, node, connector, crtc);
for crtc in crtcs {
self.connector_disconnected(niri, node, crtc);
}
let device = self.devices.remove(&node).unwrap();
@@ -576,6 +582,8 @@ impl Tty {
self.gpu_manager.as_mut().remove_node(&device.render_node);
niri.event_loop.remove(device.token);
self.refresh_ipc_outputs();
}
fn connector_connected(
@@ -608,87 +616,30 @@ impl Tty {
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() {
let wl_mode = Mode::from(*m);
debug!(
"mode: {}x{}@{:.3}",
m.size().0,
m.size().1,
wl_mode.refresh as f64 / 1000.,
);
trace!("{m:?}");
}
let mut mode = None;
if let Some(target) = &config.mode {
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);
}
let (mode, fallback) =
pick_mode(&connector, config.mode).ok_or_else(|| anyhow!("no mode"))?;
if fallback {
let target = config.mode.unwrap();
warn!(
"configured mode {}x{}{} could not be found, falling back to preferred",
target.width,
target.height,
if let Some(refresh) = target.refresh {
format!("@{refresh}")
} else {
mode = Some(m);
}
}
if mode.is_none() {
warn!(
"configured mode {}x{}{} could not be found, falling back to preferred",
target.width,
target.height,
if let Some(refresh) = target.refresh {
format!("@{refresh}")
} else {
String::new()
},
);
}
String::new()
},
);
}
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:?}");
let surface = device
.drm
.create_surface(crtc, *mode, &[connector.handle()])?;
.create_surface(crtc, mode, &[connector.handle()])?;
// Create GBM allocator.
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
@@ -711,9 +662,8 @@ impl Tty {
},
);
let wl_mode = Mode::from(*mode);
let scale = config.scale.clamp(1., 10.).ceil() as i32;
output.change_current_state(Some(wl_mode), None, Some(Scale::Integer(scale)), None);
let wl_mode = Mode::from(mode);
output.change_current_state(Some(wl_mode), None, None, None);
output.set_preferred(wl_mode);
output
@@ -785,7 +735,7 @@ impl Tty {
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
self.connectors
self.enabled_outputs
.lock()
.unwrap()
.insert(output_name.clone(), output.clone());
@@ -803,7 +753,7 @@ impl Tty {
let res = device.surfaces.insert(crtc, surface);
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.
niri.event_loop.insert_idle(move |state| {
@@ -814,25 +764,21 @@ impl Tty {
Ok(())
}
fn connector_disconnected(
&mut self,
niri: &mut Niri,
node: DrmNode,
connector: connector::Info,
crtc: crtc::Handle,
) {
debug!("disconnecting connector: {connector:?}");
fn connector_disconnected(&mut self, niri: &mut Niri, node: DrmNode, crtc: crtc::Handle) {
let Some(device) = self.devices.get_mut(&node) else {
debug!("disconnecting connector for crtc: {crtc:?}");
error!("missing device");
return;
};
let Some(surface) = device.surfaces.remove(&crtc) else {
debug!("disconnecting connector for crtc: {crtc:?}");
debug!("crtc wasn't enabled");
return;
};
debug!("disconnecting connector: {:?}", surface.name);
let output = niri
.global_space
.outputs()
@@ -847,7 +793,7 @@ impl Tty {
error!("missing output for crtc {crtc:?}");
};
self.connectors.lock().unwrap().remove(&surface.name);
self.enabled_outputs.lock().unwrap().remove(&surface.name);
}
fn on_vblank(
@@ -1210,8 +1156,66 @@ impl Tty {
}
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
fn refresh_ipc_outputs(&self) {
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")]
@@ -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)> {
@@ -1400,3 +1539,64 @@ fn queue_estimated_vblank_timer(
.unwrap();
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
View File
@@ -5,12 +5,13 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::Config;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
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::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
@@ -18,7 +19,6 @@ use smithay::reexports::winit::window::WindowBuilder;
use smithay::utils::Transform;
use super::RenderResult;
use crate::config::Config;
use crate::niri::{RedrawState, State};
use crate::utils::get_monotonic_time;
use crate::Niri;
@@ -28,7 +28,8 @@ pub struct Winit {
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
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 {
@@ -39,14 +40,6 @@ impl Winit {
.with_title("niri");
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(
"winit".to_string(),
PhysicalProperties {
@@ -61,16 +54,27 @@ impl Winit {
size: backend.window_size(),
refresh: 60_000,
};
let scale = output_config.scale.clamp(1., 10.).ceil() as i32;
output.change_current_state(
Some(mode),
Some(Transform::Flipped180),
Some(Scale::Integer(scale)),
None,
);
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
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(),
output.clone(),
)])));
@@ -90,6 +94,12 @@ impl Winit {
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());
}
WinitEvent::Input(event) => state.process_input_event(event),
@@ -109,7 +119,8 @@ impl Winit {
output,
backend,
damage_tracker,
connectors,
ipc_outputs,
enabled_outputs,
}
}
@@ -212,7 +223,11 @@ impl Winit {
}
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
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()
}
}
+197
View File
@@ -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
View File
@@ -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)]
pub struct CursorTextureCache {
@@ -243,7 +243,7 @@ impl CursorTextureCache {
scale: i32,
cursor: &XCursor,
idx: usize,
) -> TextureBuffer<GlesTexture> {
) -> Option<TextureBuffer<GlesTexture>> {
self.cache
.borrow_mut()
.entry((icon, scale))
@@ -254,7 +254,7 @@ impl CursorTextureCache {
.map(|frame| {
let _span = tracy_client::span!("create TextureBuffer");
TextureBuffer::from_memory(
let buffer = TextureBuffer::from_memory(
renderer,
&frame.pixels_rgba,
Fourcc::Abgr8888,
@@ -263,8 +263,15 @@ impl CursorTextureCache {
scale,
Transform::Normal,
None,
)
.unwrap()
);
match buffer {
Ok(x) => Some(x),
Err(err) => {
warn!("error creating a cursor texture: {err:?}");
None
}
}
})
.collect()
})[idx]
+2 -2
View File
@@ -45,7 +45,7 @@ impl DBusServers {
}
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);
let (to_niri, from_screenshot) = calloop::channel::channel();
@@ -75,7 +75,7 @@ impl DBusServers {
}
})
.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);
}
}
+41 -10
View File
@@ -4,13 +4,13 @@ use std::sync::{Arc, Mutex};
use serde::Serialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{OwnedValue, Type};
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo};
use super::Start;
pub struct DisplayConfig {
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
}
#[derive(Serialize, Type)]
@@ -53,18 +53,49 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let monitors: Vec<Monitor> = self
.connectors
let mut monitors: Vec<Monitor> = self
.enabled_outputs
.lock()
.unwrap()
.keys()
.map(|c| Monitor {
names: (c.clone(), String::new(), String::new(), String::new()),
modes: vec![],
properties: HashMap::new(),
.map(|c| {
// 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![],
properties,
}
})
.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
.iter()
.map(|m| LogicalMonitor {
@@ -85,8 +116,8 @@ impl DisplayConfig {
}
impl DisplayConfig {
pub fn new(connectors: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { connectors }
pub fn new(enabled_outputs: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { enabled_outputs }
}
}
+15 -8
View File
@@ -14,16 +14,18 @@ use super::Start;
#[derive(Clone)]
pub struct ScreenCast {
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
}
#[derive(Clone)]
pub struct Session {
id: usize,
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
}
@@ -62,6 +64,7 @@ pub enum ScreenCastToNiri {
StopCast {
session_id: usize,
},
Redraw(Output),
}
#[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 = 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 {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -149,7 +156,7 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
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()));
};
@@ -192,11 +199,11 @@ impl Stream {
impl ScreenCast {
pub fn new(
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
connectors,
enabled_outputs,
to_niri,
sessions: Arc::new(Mutex::new(vec![])),
}
@@ -221,12 +228,12 @@ impl Start for ScreenCast {
impl Session {
pub fn new(
id: usize,
connectors: Arc<Mutex<HashMap<String, Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
connectors,
enabled_outputs,
streams: Arc::new(Mutex::new(vec![])),
to_niri,
}
+7 -3
View File
@@ -25,9 +25,13 @@ impl ServiceChannel {
}
let (sock1, sock2) = UnixStream::pair().unwrap();
self.display
.insert_client(sock2, Arc::new(ClientState::default()))
.unwrap();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
// 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()) })
}
}
+162
View File
@@ -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
View File
@@ -22,6 +22,9 @@ use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
};
use smithay::wayland::selection::data_device::{
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
ServerDndGrabHandler,
@@ -38,11 +41,11 @@ use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_session_lock, delegate_tablet_manager,
delegate_text_input_manager, delegate_virtual_keyboard_manager,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
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;
impl SeatHandler for State {
@@ -251,3 +254,26 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
});
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
View File
@@ -1,7 +1,9 @@
use smithay::desktop::{
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::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
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::wayland::compositor::{send_surface_state, with_states};
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::{
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 crate::niri::State;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
impl XdgShellHandler for State {
@@ -90,8 +93,93 @@ impl XdgShellHandler for State {
surface.send_repositioned(token);
}
fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {
// FIXME popup grabs
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
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) {
@@ -116,9 +204,6 @@ impl XdgShellHandler for State {
.capabilities
.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
.niri
.layout
@@ -192,40 +277,42 @@ delegate_xdg_shell!(State);
impl XdgDecorationHandler for State {
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
// If we want CSD, we hide this global altogether.
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) {
// 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| {
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) {
toplevel.send_pending_configure();
toplevel.send_configure();
}
}
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
// If we want CSD, we hide this global altogether.
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) {
toplevel.send_pending_configure();
toplevel.send_configure();
}
}
}
+429
View File
@@ -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()
}
}
+196 -16
View File
@@ -1,6 +1,7 @@
use std::any::Any;
use std::collections::HashSet;
use niri_config::{Action, Binds, LayoutAction, Modifiers};
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
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::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use crate::config::{Action, Binds, LayoutAction, Modifiers};
use crate::niri::State;
use crate::screenshot_ui::ScreenshotUi;
use crate::utils::{center, get_monotonic_time, spawn};
@@ -54,6 +54,16 @@ impl State {
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::*;
match event {
DeviceAdded { device } => self.on_device_added(device),
@@ -82,6 +92,18 @@ impl State {
TouchFrame { .. } => (),
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>) {
@@ -89,14 +111,7 @@ impl State {
match event {
InputEvent::DeviceAdded { 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 = &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);
}
self.niri.devices.insert(device.clone());
if device.has_capability(input::DeviceCapability::TabletTool) {
match device.size() {
@@ -110,9 +125,12 @@ impl State {
}
}
}
apply_libinput_settings(&self.niri.config.borrow().input, device);
}
InputEvent::DeviceRemoved { device } => {
self.niri.tablets.remove(device);
self.niri.devices.remove(device);
}
_ => (),
}
@@ -199,6 +217,13 @@ impl State {
let key_code = event.key_code();
let modified = keysym.modified_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(
&mut this.niri.suppressed_keys,
bindings,
@@ -227,8 +252,14 @@ impl State {
match action {
Action::Quit => {
info!("quitting because quit bind was pressed");
self.niri.stop_signal.stop()
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");
self.niri.stop_signal.stop()
}
}
Action::ChangeVt(vt) => {
self.backend.change_vt(vt);
@@ -311,6 +342,8 @@ impl State {
let focus = self.niri.layout.focus().cloned();
if let Some(window) = focus {
self.niri.layout.toggle_fullscreen(&window);
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::SwitchLayout(action) => {
@@ -364,21 +397,33 @@ impl State {
}
Action::FocusColumnLeft => {
self.niri.layout.focus_left();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnRight => {
self.niri.layout.focus_right();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnFirst => {
self.niri.layout.focus_column_first();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLast => {
self.niri.layout.focus_column_last();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowDown => {
self.niri.layout.focus_down();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUp => {
self.niri.layout.focus_up();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceDown => {
self.niri.layout.focus_window_or_workspace_down();
@@ -406,6 +451,22 @@ impl State {
// FIXME: granular
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 => {
self.niri.layout.switch_workspace_down();
// FIXME: granular
@@ -501,12 +562,41 @@ impl State {
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) => {
self.niri.layout.set_column_width(change);
}
Action::SetWindowHeight(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();
if ButtonState::Pressed == button_state && !pointer.is_grabbed() {
if ButtonState::Pressed == button_state {
if let Some(window) = self.niri.window_under_cursor() {
let window = window.clone();
self.niri.layout.activate_window(&window);
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some(output) = self.niri.output_under_cursor() {
self.niri.layout.activate_output(&output);
// FIXME: granular.
self.niri.queue_redraw_all();
}
};
@@ -1280,9 +1376,7 @@ fn action(
comp_mod = Modifiers::empty();
}
let Some(raw) = raw else {
return None;
};
let raw = raw?;
for bind in &bindings.0 {
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 {
matches!(
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)]
mod tests {
use niri_config::{Action, Bind, Binds, Key, Modifiers};
use super::*;
use crate::config::{Action, Bind, Binds, Key, Modifiers};
#[test]
fn bindings_suppress_keys() {
+106
View File
@@ -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(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod client;
pub mod server;
+127
View File
@@ -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(())
}
+3 -4
View File
@@ -1,12 +1,11 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::{self, Color};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Scale, Size};
use crate::config::{self, Color};
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
@@ -21,7 +20,7 @@ pub struct FocusRing {
pub type FocusRingRenderElement = SolidColorRenderElement;
impl FocusRing {
pub fn new(config: config::FocusRing) -> Self {
pub fn new(config: niri_config::FocusRing) -> Self {
Self {
buffers: 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.width = config.width.into();
self.active_color = config.active_color;
+276 -20
View File
@@ -33,6 +33,7 @@ use std::mem;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{self, CenterFocusedColumn, Config, SizeChange, Struts};
use smithay::backend::renderer::element::AsRenderElements;
use smithay::backend::renderer::{ImportAll, Renderer};
use smithay::desktop::space::SpaceElement;
@@ -48,10 +49,9 @@ use smithay::wayland::shell::xdg::SurfaceCachedState;
pub use self::monitor::MonitorRenderElement;
use self::monitor::{Monitor, WorkspaceSwitch, WorkspaceSwitchGesture};
use self::workspace::{
compute_working_area, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
};
use crate::animation::Animation;
use crate::config::{self, Config, SizeChange, Struts};
use crate::utils::output_size;
mod focus_ring;
@@ -137,8 +137,9 @@ pub struct Options {
gaps: i32,
/// Extra padding around the working area in logical pixels.
struts: Struts,
focus_ring: config::FocusRing,
border: config::FocusRing,
focus_ring: niri_config::FocusRing,
border: niri_config::FocusRing,
center_focused_column: CenterFocusedColumn,
/// Column widths that `toggle_width()` switches between.
preset_widths: Vec<ColumnWidth>,
/// Initial width for new columns.
@@ -151,7 +152,8 @@ impl Default for Options {
gaps: 16,
struts: Default::default(),
focus_ring: Default::default(),
border: config::default_border(),
border: niri_config::default_border(),
center_focused_column: Default::default(),
preset_widths: vec![
ColumnWidth::Proportion(1. / 3.),
ColumnWidth::Proportion(0.5),
@@ -164,7 +166,8 @@ impl Default for Options {
impl Options {
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() {
Options::default().preset_widths
@@ -178,17 +181,18 @@ impl Options {
// Missing default_column_width maps to Some(ColumnWidth::Proportion(0.5)),
// while present, but empty, maps to None.
let default_width = config
let default_width = layout
.default_column_width
.as_ref()
.map(|w| w.0.first().copied().map(ColumnWidth::from))
.unwrap_or(Some(ColumnWidth::Proportion(0.5)));
Self {
gaps: config.gaps.into(),
struts: config.struts,
focus_ring: config.focus_ring,
border: config.border,
gaps: layout.gaps.into(),
struts: layout.struts,
focus_ring: layout.focus_ring,
border: layout.border,
center_focused_column: layout.center_focused_column,
preset_widths,
default_width,
}
@@ -306,11 +310,21 @@ impl<W: LayoutElement> Layout<W> {
} => {
let primary = &mut monitors[primary_idx];
let mut stopped_primary_ws_switch = false;
let mut workspaces = vec![];
for i in (0..primary.workspaces.len()).rev() {
if primary.workspaces[i].original_output == id {
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
// another monitor. However, we will add an empty workspace in the end
// 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();
// 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.
///
/// 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()
&& idx != mon.active_workspace_idx
&& idx != mon.workspaces.len() - 1
&& mon.workspace_switch.is_none()
{
mon.workspaces.remove(idx);
@@ -610,6 +654,8 @@ impl<W: LayoutElement> Layout<W> {
}
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 {
panic!()
};
@@ -621,6 +667,7 @@ impl<W: LayoutElement> Layout<W> {
for ws in &mut mon.workspaces {
ws.set_view_size(view_size, working_area);
ws.update_output_scale_transform();
}
break;
@@ -893,6 +940,27 @@ impl<W: LayoutElement> Layout<W> {
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) {
let Some(monitor) = self.active_monitor() else {
return;
@@ -1010,6 +1078,14 @@ impl<W: LayoutElement> Layout<W> {
"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);
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) {
if !matches!(&self.monitor_set, MonitorSet::Normal { .. }) {
return;
@@ -1604,9 +1704,13 @@ mod tests {
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
MoveWorkspaceDown,
MoveWorkspaceUp,
MoveWindowToOutput(#[proptest(strategy = "1..=5u8")] u8),
MoveColumnToOutput(#[proptest(strategy = "1..=5u8")] u8),
SwitchPresetColumnWidth,
MaximizeColumn,
SetColumnWidth(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
@@ -1725,6 +1829,9 @@ mod tests {
Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(),
Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(),
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) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
@@ -1733,6 +1840,14 @@ mod tests {
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::MoveWorkspaceUp => layout.move_workspace_up(),
Op::SwitchPresetColumnWidth => layout.toggle_width(),
@@ -1851,6 +1966,11 @@ mod tests {
Op::MoveWindowToWorkspace(1),
Op::MoveWindowToWorkspace(2),
Op::MoveWindowToWorkspace(3),
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceUp,
Op::MoveColumnToWorkspace(1),
Op::MoveColumnToWorkspace(2),
Op::MoveColumnToWorkspace(3),
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
Op::MoveWindowUp,
@@ -1973,6 +2093,11 @@ mod tests {
Op::MoveWindowToWorkspace(1),
Op::MoveWindowToWorkspace(2),
Op::MoveWindowToWorkspace(3),
Op::MoveColumnToWorkspaceDown,
Op::MoveColumnToWorkspaceUp,
Op::MoveColumnToWorkspace(1),
Op::MoveColumnToWorkspace(2),
Op::MoveColumnToWorkspace(3),
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
Op::MoveWindowUp,
@@ -2186,8 +2311,145 @@ mod tests {
check_ops_with_options(options, &ops);
}
fn arbitrary_border() -> impl Strategy<Value = u16> {
prop_oneof![Just(0), (1..=u16::MAX)]
#[test]
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! {
@@ -2202,13 +2464,7 @@ mod tests {
})]
#[test]
fn random_operations_dont_panic(ops: Vec<Op>, border in arbitrary_border()) {
let mut options = Options::default();
if border != 0 {
options.border.off = false;
options.border.width = border;
}
fn random_operations_dont_panic(ops: Vec<Op>, options in arbitrary_options()) {
// eprintln!("{ops:?}");
check_ops_with_options(options, &ops);
}
+78 -3
View File
@@ -2,6 +2,7 @@ use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;
use niri_config::SizeChange;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
@@ -11,11 +12,10 @@ use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use super::workspace::{
compute_working_area, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::config::SizeChange;
use crate::utils::output_size;
#[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());
for idx in (0..self.workspaces.len() - 1).rev() {
@@ -323,6 +342,62 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
pub fn move_column_to_workspace_up(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = source_workspace_idx.saturating_sub(1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
}
pub fn move_column_to_workspace_down(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
}
pub fn move_column_to_workspace(&mut self, idx: usize) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn switch_workspace_up(&mut self) {
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
}
+175 -48
View File
@@ -3,6 +3,7 @@ use std::iter::zip;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, PresetWidth, SizeChange, Struts};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::utils::RelocateRenderElement;
use smithay::backend::renderer::{ImportAll, Renderer};
@@ -17,7 +18,6 @@ use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::tile::Tile;
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::config::{PresetWidth, SizeChange, Struts};
use crate::utils::output_size;
#[derive(Debug)]
@@ -308,7 +308,7 @@ impl<W: LayoutElement> Workspace<W> {
fn enter_output_for_window(&self, window: &W) {
if let Some(output) = &self.output {
prepare_for_output(window, output);
set_preferred_scale_transform(window, 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> {
let mut border = 0;
if !self.options.border.off {
@@ -363,7 +372,7 @@ impl<W: LayoutElement> Workspace<W> {
let bounds = self.toplevel_bounds();
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| {
@@ -397,9 +406,7 @@ impl<W: LayoutElement> Workspace<W> {
new_offset - self.working_area.loc.x
}
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);
fn animate_view_offset(&mut self, current_x: i32, idx: usize, new_view_offset: i32) {
let new_col_x = self.column_x(idx);
let from_view_offset = current_x - new_col_x;
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) {
if self.active_column_idx == idx {
return;
}
let current_x = self.view_pos();
self.animate_view_offset_to_column(current_x, idx);
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);
} 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;
@@ -488,15 +560,58 @@ impl<W: LayoutElement> Workspace<W> {
width,
is_full_width,
);
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 {
// 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);
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.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;
}
@@ -549,6 +664,42 @@ impl<W: LayoutElement> Workspace<W> {
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) {
let column_idx = self
.columns
@@ -574,7 +725,14 @@ impl<W: LayoutElement> Workspace<W> {
if idx == self.active_column_idx {
// We might need to move the view to ensure the resized window is still visible.
let current_x = self.view_pos();
self.animate_view_offset_to_column(current_x, idx);
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);
}
}
}
@@ -654,6 +812,7 @@ impl<W: LayoutElement> Workspace<W> {
let column = self.columns.remove(self.active_column_idx);
self.columns.insert(new_idx, column);
// FIXME: should this be different when always centering?
self.view_offset =
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 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];
target_column.add_window(window);
@@ -738,42 +898,8 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn center_column(&mut self) {
if self.columns.is_empty() {
return;
}
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),
));
let center_x = self.view_pos();
self.animate_view_offset_to_column_centered(center_x, self.active_column_idx);
}
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 transform = output.current_transform();
window.set_preferred_scale_transform(scale, transform);
+81 -19
View File
@@ -3,13 +3,16 @@ extern crate tracing;
mod animation;
mod backend;
mod config;
mod config_error_notification;
mod cursor;
#[cfg(feature = "dbus")]
mod dbus;
mod exit_confirm_dialog;
mod frame_clock;
mod handlers;
mod hotkey_overlay;
mod input;
mod ipc;
mod layout;
mod niri;
mod render_helpers;
@@ -28,12 +31,12 @@ use std::process::Command;
use std::{env, mem};
use clap::{Parser, Subcommand};
use config::Config;
use directories::ProjectDirs;
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
use git_version::git_version;
use miette::{Context, NarratableReportHandler};
use niri::{Niri, State};
use niri_config::Config;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::{self, EventLoop};
@@ -42,10 +45,11 @@ use tracing_subscriber::EnvFilter;
use utils::spawn;
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)]
#[command(author, version, about, long_about = None)]
#[command(author, version = version(), about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_value_name = "SUBCOMMAND")]
#[command(subcommand_help_heading = "Subcommands")]
@@ -69,6 +73,22 @@ enum Sub {
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
msg: Msg,
/// Format output as JSON.
#[arg(short, long)]
json: bool,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
enum Msg {
/// List connected outputs.
Outputs,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -110,33 +130,45 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let _client = tracy_client::Client::start();
// 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.
if let Some(subcommand) = cli.subcommand {
match subcommand {
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");
return Ok(());
}
Sub::Msg { msg, json } => {
handle_msg(msg, json)?;
return Ok(());
}
Sub::Panic => cause_panic(),
}
}
info!(
"starting version {} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
);
info!("starting version {}", &version());
// Load the config.
let (mut config, path) = match Config::load(cli.config).context("error loading config") {
Ok((config, path)) => (config, Some(path)),
Err(err) => {
warn!("{err:?}");
(Config::default(), None)
}
};
let path = cli.config.or_else(default_config_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) => {
warn!("{err:?}");
config_errored = true;
None
}
})
.unwrap_or_default();
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
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()
);
// 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 {
// We're starting as a systemd service. Export our variables.
import_env_to_systemd();
@@ -202,6 +240,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
spawn(elem.command);
}
// Show the config error notification right away if needed.
if config_errored {
state.niri.config_error_notification.show();
}
// Run the compositor.
event_loop
.run(None, &mut state, |state| state.refresh_and_flush_clients())
@@ -210,6 +253,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
)
}
fn import_env_to_systemd() {
let rv = Command::new("/bin/sh")
.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)
}
+444 -97
View File
@@ -10,6 +10,8 @@ use std::{env, mem, thread};
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode;
use anyhow::Context;
use calloop::futures::Scheduler;
use niri_config::{Config, TrackLayout};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::surface::{
@@ -34,12 +36,13 @@ use smithay::desktop::utils::{
under_from_surface_tree, update_surface_primary_scanout_output, OutputPresentationFeedback,
};
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::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus, MotionEvent};
use smithay::input::{Seat, SeatState};
use smithay::output::Output;
use smithay::output::{self, Output};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
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::presentation::PresentationState;
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::primary_selection::PrimarySelectionState;
use smithay::wayland::selection::wlr_data_control::DataControlState;
@@ -86,15 +90,20 @@ use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState;
use crate::animation;
use crate::backend::tty::{SurfaceDmabufFeedback, TtyFrame, TtyRenderer, TtyRendererError};
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};
#[cfg(feature = "dbus")]
use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
#[cfg(feature = "xdp-gnome-screencast")]
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
use crate::exit_confirm_dialog::ExitConfirmDialog;
use crate::frame_clock::FrameClock;
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::pw_utils::{Cast, PipeWire};
use crate::render_helpers::{NiriRenderer, PrimaryGpuTextureRenderElement};
@@ -110,6 +119,7 @@ pub struct Niri {
pub config: Rc<RefCell<Config>>,
pub event_loop: LoopHandle<'static, State>,
pub scheduler: Scheduler<()>,
pub stop_signal: LoopSignal,
pub display_handle: DisplayHandle,
pub socket_name: OsString,
@@ -133,6 +143,7 @@ pub struct Niri {
// When false, we're idling with monitors powered off.
pub monitors_active: bool,
pub devices: HashSet<input::Device>,
pub tablets: HashMap<input::Device, TabletData>,
// Smithay state.
@@ -157,11 +168,17 @@ pub struct Niri {
pub primary_selection_state: PrimarySelectionState,
pub data_control_state: DataControlState,
pub popups: PopupManager,
pub popup_grab: Option<PopupGrabState>,
pub presentation_state: PresentationState,
pub security_context_state: SecurityContextState,
pub seat: Seat<State>,
/// Scancodes of the keys to suppress.
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_texture_cache: CursorTextureCache,
@@ -173,12 +190,17 @@ pub struct Niri {
pub lock_state: LockState,
pub screenshot_ui: ScreenshotUi,
pub config_error_notification: ConfigErrorNotification,
pub hotkey_overlay: HotkeyOverlay,
pub exit_confirm_dialog: Option<ExitConfirmDialog>,
#[cfg(feature = "dbus")]
pub dbus: Option<crate::dbus::DBusServers>,
#[cfg(feature = "dbus")]
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).
pub casts: Vec<Cast>,
pub pipewire: Option<PipeWire>,
@@ -222,6 +244,11 @@ pub enum RedrawState {
WaitingForEstimatedVBlankAndQueued((RegistrationToken, Idle<'static>)),
}
pub struct PopupGrabState {
pub root: WlSurface,
pub grab: PopupGrab<State>,
}
#[derive(Clone, PartialEq, Eq)]
pub struct PointerFocus {
pub output: Output,
@@ -299,7 +326,8 @@ impl State {
self.niri.cursor_manager.check_cursor_image_surface_alive();
self.niri.refresh_pointer_outputs();
self.niri.popups.cleanup();
self.update_focus();
self.niri.refresh_popup_grab();
self.update_keyboard_focus();
self.refresh_pointer_focus();
{
@@ -392,7 +420,7 @@ impl State {
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() {
self.niri.lock_surface_focus()
} 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 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 = || {
self.niri
.layout
@@ -413,7 +452,14 @@ impl State {
.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() {
surface = surface.or_else(layout_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 current_focus = keyboard.current_focus();
if current_focus != focus {
if self.niri.keyboard_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 {
let current_layout =
keyboard.with_xkb_state(self, |context| context.active_layout());
let mut new_layout = current_layout;
// 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| {
let cell = data
.data_map
@@ -463,6 +533,7 @@ impl State {
}
}
self.niri.keyboard_focus = focus.clone();
keyboard.set_focus(self, focus, SERIAL_COUNTER.next_serial());
// FIXME: can be more granular.
@@ -473,18 +544,24 @@ impl State {
pub fn reload_config(&mut self, path: PathBuf) {
let _span = tracy_client::span!("State::reload_config");
let config = match Config::load(Some(path)) {
Ok((config, _)) => config,
let config = match Config::load(&path) {
Ok(config) => config,
Err(err) => {
warn!("{:?}", err.context("error loading config"));
self.niri.config_error_notification.show();
self.niri.queue_redraw_all();
return;
}
};
self.niri.config_error_notification.hide();
self.niri.layout.update_config(&config);
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
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();
// 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;
// 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();
// FIXME: apply output scale and whatnot.
// FIXME: apply libinput device settings.
// FIXME: apply xdg decoration settings.
}
#[cfg(feature = "xdp-gnome-screencast")]
@@ -574,6 +707,7 @@ impl State {
}
}
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 {
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 config_ = config.borrow();
@@ -640,18 +777,32 @@ impl Niri {
&display_handle,
[WmCapabilities::Fullscreen],
);
let xdg_decoration_state = XdgDecorationState::new::<State>(&display_handle);
let kde_decoration_state = KdeDecorationState::new::<State>(
let xdg_decoration_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,
if config_.prefer_no_csd {
KdeDecorationsMode::Server
} else {
KdeDecorationsMode::Client
// If we want CSD we will hide the global.
KdeDecorationsMode::Server,
|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 =
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 output_manager_state =
OutputManagerState::new_with_xdg_output::<State>(&display_handle);
@@ -666,16 +817,24 @@ impl Niri {
let data_control_state = DataControlState::new::<State, _>(
&display_handle,
Some(&primary_selection_state),
|_| true,
|client| !client.get_data::<ClientState>().unwrap().restricted,
);
let presentation_state =
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 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 =
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());
seat.add_keyboard(
@@ -708,18 +867,46 @@ impl Niri {
});
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_name = socket_source.socket_name().to_os_string();
event_loop
.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) {
error!("error inserting client: {err}");
}
})
.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) {
Ok(pipewire) => Some(pipewire),
Err(err) => {
@@ -744,6 +931,7 @@ impl Niri {
config,
event_loop,
scheduler,
stop_signal,
socket_name,
display_handle,
@@ -756,6 +944,7 @@ impl Niri {
unmapped_windows: HashMap::new(),
monitors_active: true,
devices: HashSet::new(),
tablets: HashMap::new(),
compositor_state,
@@ -779,10 +968,13 @@ impl Niri {
primary_selection_state,
data_control_state,
popups: PopupManager::default(),
popup_grab: None,
suppressed_keys: HashSet::new(),
presentation_state,
security_context_state,
seat,
keyboard_focus: None,
cursor_manager,
cursor_texture_cache: Default::default(),
cursor_shape_manager_state,
@@ -793,12 +985,17 @@ impl Niri {
lock_state: LockState::Unlocked,
screenshot_ui,
config_error_notification,
hotkey_overlay,
exit_confirm_dialog,
#[cfg(feature = "dbus")]
dbus: None,
#[cfg(feature = "dbus")]
inhibit_power_key_fd: None,
ipc_server,
pipewire,
casts: vec![],
}
@@ -824,72 +1021,143 @@ impl Niri {
Ok(())
}
/// Repositions all outputs, optionally adding a new output.
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 position = self.global_space.output_geometry(output).map(|geo| geo.loc);
let config = config
.outputs
.iter()
.find(|o| o.name == name)
.and_then(|c| c.position);
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 new_position = config
.map(|pos| Point::from((pos.x, pos.y)))
.filter(|pos| {
// Ensure that the requested position does not overlap any existing output.
let target_geom = Rectangle::from_loc_and_size(*pos, size);
let overlap = self
.global_space
.outputs()
.map(|output| self.global_space.output_geometry(output).unwrap())
.find(|geom| geom.overlaps(target_geom));
if let Some(overlap) = overlap {
warn!(
"output {name} at x={} y={} sized {}x{} \
overlaps an existing output at x={} y={} sized {}x{}, \
falling back to automatic placement",
pos.x,
pos.y,
size.w,
size.h,
overlap.loc.x,
overlap.loc.y,
overlap.size.w,
overlap.size.h,
);
false
} else {
true
}
})
.unwrap_or_else(|| {
let x = self
.global_space
.outputs()
.map(|output| self.global_space.output_geometry(output).unwrap())
.map(|geom| geom.loc.x + geom.size.w)
.max()
.unwrap_or(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!(
"putting output {name} at x={} y={}",
new_position.x, new_position.y
);
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 config = self
let scale = self
.config
.borrow()
.outputs
.iter()
.find(|o| o.name == name)
.cloned()
.unwrap_or_default();
.map(|c| c.scale)
.unwrap_or(1.);
let scale = scale.clamp(1., 10.).ceil() as i32;
let size = output_size(&output);
let position = config
.position
.map(|pos| Point::from((pos.x, pos.y)))
.filter(|pos| {
// Ensure that the requested position does not overlap any existing output.
let target_geom = Rectangle::from_loc_and_size(*pos, size);
// 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);
let overlap = self
.global_space
.outputs()
.map(|output| self.global_space.output_geometry(output).unwrap())
.find(|geom| geom.overlaps(target_geom));
if let Some(overlap) = overlap {
warn!(
"new output {name} at x={} y={} sized {}x{} \
overlaps an existing output at x={} y={} sized {}x{}, \
falling back to automatic placement",
pos.x,
pos.y,
size.w,
size.h,
overlap.loc.x,
overlap.loc.y,
overlap.size.w,
overlap.size.h,
);
false
} else {
true
}
})
.unwrap_or_else(|| {
let x = self
.global_space
.outputs()
.map(|output| self.global_space.output_geometry(output).unwrap())
.map(|geom| geom.loc.x + geom.size.w)
.max()
.unwrap_or(0);
Point::from((x, 0))
});
debug!(
"putting new output {name} at x={} y={}",
position.x, position.y
);
self.global_space.map_output(&output, position);
self.layout.add_output(output.clone());
output.change_current_state(None, None, None, Some(position));
let lock_render_state = if self.is_locked() {
// We haven't rendered anything yet so it's as good as locked.
@@ -898,6 +1166,7 @@ impl Niri {
LockRenderState::Unlocked
};
let size = output_size(&output);
let state = OutputState {
global,
redraw_state: RedrawState::Idle,
@@ -911,14 +1180,21 @@ impl Niri {
};
let rv = self.output_state.insert(output.clone(), state);
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");
// 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) {
for layer in layer_map_for_output(output).layers() {
layer.layer_surface().send_close();
}
self.layout.remove_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();
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 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_mode = output.current_mode().unwrap();
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.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
@@ -1055,6 +1334,19 @@ impl Niri {
}
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)?;
Some(window)
}
@@ -1257,7 +1549,7 @@ impl Niri {
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 = output_under_cursor
.as_ref()
@@ -1268,6 +1560,14 @@ impl Niri {
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.
pub fn queue_redraw_all(&mut self) {
let outputs: Vec<_> = self.output_state.keys().cloned().collect();
@@ -1365,16 +1665,19 @@ impl Niri {
idx,
);
let pointer_elements = vec![OutputRenderElements::NamedPointer(
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
pointer_pos.to_f64(),
&texture,
None,
None,
None,
Kind::Cursor,
)),
)];
let mut pointer_elements = vec![];
if let Some(texture) = texture {
pointer_elements.push(OutputRenderElements::NamedPointer(
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
pointer_pos.to_f64(),
&texture,
None,
None,
None,
Kind::Cursor,
)),
));
}
(pointer_elements, pointer_pos)
}
@@ -1522,6 +1825,18 @@ impl Niri {
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 self.is_locked() {
let state = self.output_state.get(output).unwrap();
@@ -1577,6 +1892,11 @@ impl Niri {
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.
let mon = self.layout.monitor_for_output(output).unwrap();
let monitor_elements = mon.render_elements(renderer);
@@ -1649,6 +1969,11 @@ impl Niri {
.unwrap()
.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.
state.unfinished_animations_remain |= self
.cursor_manager
@@ -2440,9 +2765,11 @@ impl Niri {
}
}
#[derive(Default)]
pub struct ClientState {
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 {
@@ -2560,6 +2887,7 @@ pub enum OutputRenderElements<R: NiriRenderer> {
NamedPointer(PrimaryGpuTextureRenderElement),
SolidColor(SolidColorRenderElement),
ScreenshotUi(ScreenshotUiRenderElement),
ConfigErrorNotification(ConfigErrorNotificationRenderElement<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::SolidColor(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::SolidColor(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::SolidColor(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::SolidColor(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::SolidColor(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::SolidColor(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::SolidColor(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::SolidColor(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::SolidColor(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) => {
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::SolidColor(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) => {
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::SolidColor(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)
}
}
impl<R: NiriRenderer> From<ConfigErrorNotificationRenderElement<R>> for OutputRenderElements<R> {
fn from(x: ConfigErrorNotificationRenderElement<R>) -> Self {
Self::ConfigErrorNotification(x)
}
}
+11 -1
View File
@@ -95,11 +95,20 @@ impl PipeWire {
) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast");
let to_niri_ = to_niri.clone();
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:?}");
}
};
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 size = mode.size;
@@ -158,6 +167,7 @@ impl PipeWire {
StreamState::Connecting => (),
StreamState::Streaming => {
is_active.set(true);
redraw();
}
}
}
+5 -2
View File
@@ -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::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
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 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.
pub trait NiriRenderer:
ImportAll
+ ImportMem
+ ExportMem
+ Bind<Dmabuf>
+ Offscreen<GlesTexture>
@@ -28,7 +31,7 @@ pub trait NiriRenderer:
impl<R> NiriRenderer for R
where
R: ImportAll + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
+8 -4
View File
@@ -5,6 +5,7 @@ use std::mem;
use anyhow::Context;
use arrayvec::ArrayVec;
use niri_config::Action;
use smithay::backend::allocator::Fourcc;
use smithay::backend::input::{ButtonState, MouseButton};
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 crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::config::Action;
use crate::render_helpers::PrimaryGpuTextureRenderElement;
const BORDER: i32 = 2;
@@ -41,6 +41,7 @@ pub enum ScreenshotUi {
pub struct OutputData {
size: Size<i32, Physical>,
scale: i32,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
buffers: [SolidColorBuffer; 8],
@@ -106,10 +107,11 @@ impl ScreenshotUi {
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
let scale = output.current_scale().integer_scale();
let texture_buffer = TextureBuffer::from_texture(
renderer,
texture.clone(),
output.current_scale().integer_scale(),
scale,
Transform::Normal,
None,
);
@@ -126,6 +128,7 @@ impl ScreenshotUi {
let locations = [Default::default(); 8];
let data = OutputData {
size,
scale,
texture,
texture_buffer,
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 {
Some(output_data.get(output)?.size)
let data = output_data.get(output)?;
Some((data.size, data.scale))
} else {
None
}
+8 -2
View File
@@ -11,12 +11,11 @@ use std::time::Duration;
use anyhow::{ensure, Context};
use directories::UserDirs;
use niri_config::Config;
use smithay::output::Output;
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
use smithay::utils::{Logical, Point, Rectangle, Size};
use crate::config::Config;
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
(t.0.clone(), t.1.clone())
}
@@ -191,3 +190,10 @@ pub fn show_screenshot_notification(image_path: Option<PathBuf>) {
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;
}