Compare commits

..

117 Commits

Author SHA1 Message Date
Ivan Molodetskikh 72c8f569ac Bump version to 0.1.1 2024-02-03 10:00:06 +04:00
Ivan Molodetskikh 798d9c55df Support fullscreen for new windows 2024-02-03 09:45:26 +04:00
Ivan Molodetskikh 05613eed1e Verify that pending fullscreen matches column 2024-02-03 09:44:34 +04:00
Ivan Molodetskikh b23dd4b800 Respect natural-scroll for workspace switch gesture 2024-02-03 09:00:08 +04:00
Ivan Molodetskikh 1f72089a46 Place new workspace after current when moving
This feels more natural, also makes moving back and forth idempotent in
most cases.
2024-02-03 08:42:56 +04:00
Ivan Molodetskikh fbe9020915 Update dependencies 2024-02-02 17:04:17 +04:00
Ivan Molodetskikh 2036116f16 config: Premultiply alpha in Color when converting to f32
Smithay wants premultiplied alpha.
2024-02-01 18:53:45 +04:00
Ivan Molodetskikh 9afd728ae9 Add error messages to backend initialization 2024-02-01 16:55:46 +04:00
Andreas Stührk e51268a39e Add actions to move the active workspace to another monitor 2024-02-01 12:29:46 +04:00
Ivan Molodetskikh 0a715ce155 default-config: Improve wording for focus-ring/border comment
SSD or server-side decorations is never mentioned elsewhere.
2024-02-01 12:06:13 +04:00
Ivan Molodetskikh 89ac958670 default-config: Document how focus ring and border draw behind
Related: https://github.com/YaLTeR/niri/issues/150
2024-02-01 10:08:15 +04:00
Ivan Molodetskikh 2e50f8dee0 Hardcode winit transform for now 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 7052f0129e Stop screencasts on size changes 2024-01-31 23:02:38 +04:00
axtloss 962e159db6 Add option to rotate outputs 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 11bff3a2f1 Update Smithay (rotation fix) 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 15606304f2 README: Bring AUR link back 2024-01-30 22:36:30 -08:00
Christian Meissl 85eac9d9d0 chore: bump smithay
includes fixes for wrong direct scan-out transform
and damage artifacts on output transform changes.
also includes a fix for a race in popup surface re-use.
2024-01-30 15:30:31 +04:00
Ivan Molodetskikh d3f4583c90 foreign_toplevel: Use OutputHandler to send output_enter on demand 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh fefb1cccd6 foreign_toplevel: Update the focused window last 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh deef52519a foreign_toplevel: Change activated to mean keyboard focus 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh 59ff331597 Implement wlr-foreign-toplevel-management
The parent event isn't sent but whatever.
2024-01-30 12:30:57 +04:00
Christian Meissl b813f99abd tty: reset surface state after changing monitor state
changing the "ACTIVE" property of a surface requires
to re-evaluate the surface state.
2024-01-30 08:03:21 +04:00
Ivan Molodetskikh d9b9cec8b8 README: Remove AUR link for now
It doesn't work properly yet apparently.
2024-01-29 12:29:32 -08:00
Christian Meissl 597ea62d17 input: update keyboard led state 2024-01-28 23:43:08 +04:00
Ivan Molodetskikh 51243a0a50 Show notification about creating a default config 2024-01-28 17:15:47 +04:00
Ivan Molodetskikh 0ebcc3e0d6 Create default config file if missing 2024-01-28 17:15:33 +04:00
Ivan Molodetskikh 64c85d865e winit: Don't remove output on CloseRequested
More winit events can process after CloseRequested, which will cause a
panic if trying to access the now-removed output.
2024-01-28 16:30:29 +04:00
Ivan Molodetskikh 367e4955ea Mark Msg as pub
Seems to break the build on 1.72.0 otherwise.
2024-01-28 09:34:42 +04:00
Ivan Molodetskikh dd967554d1 Bump version to 0.1.0 2024-01-27 14:10:31 +04:00
Ivan Molodetskikh 6d7c220137 Try harder to find an output for the screenshot UI
The mouse might be outside any outputs, let's try to open in that case
anyway.
2024-01-27 14:09:55 +04:00
Ivan Molodetskikh d77aac1afa Fix damage when rendering to texture 2024-01-27 10:50:40 +04:00
Ivan Molodetskikh 837a0a20fb Update README 2024-01-25 08:34:42 +04:00
Ivan Molodetskikh ecdf756b55 Name output render element better 2024-01-25 08:02:33 +04:00
Christian Meissl 73f3c160b2 use pixman for cursor plane rendering 2024-01-25 07:49:51 +04:00
Christian Meissl 5f99eb13ab Remove hack for fixed EGLDisplay issue 2024-01-25 07:49:51 +04:00
Christian Meissl 20326b093c Update smithay 2024-01-25 07:49:51 +04:00
Ivan Molodetskikh 467d92a4b4 github: Add a feature request link to start a discussion 2024-01-23 17:41:35 +04:00
Ivan Molodetskikh 15bb69c0b9 Update issue templates 2024-01-23 05:36:19 -08:00
Ivan Molodetskikh adfbfdffb3 Create a bug report template 2024-01-23 05:34:38 -08:00
Ivan Molodetskikh 087ed260c5 Update Smithay (find_popup_root_surface() panic fix) 2024-01-23 17:12:47 +04:00
Ivan Molodetskikh f5642ab733 Ignore popup grabs when IME keyboard grab is active
Doing this properly will require more refactors, potentially in Smithay.
For now let's just ignore popup grabs to make popups work.
2024-01-23 17:05:08 +04:00
Ivan Molodetskikh ab9706cb30 screencast: Emit MonitorsChanged 2024-01-23 12:02:52 +04:00
Ivan Molodetskikh 05f2a3709b srceencast: Send stream size
Kooha requires this (even though it's optional). Unfortunately, Kooha
also seems to want memfd recording so it doesn't work anyway.
2024-01-23 11:36:11 +04:00
Ivan Molodetskikh 743173ef64 config: Bump precision on the default widths
This seems to actually matter on my 2560x display.
2024-01-22 20:43:33 +04:00
Ivan Molodetskikh cbbb7a26fc Update Smithay, use device changed session resume code
Should fix most cases of monitors failing to light up after a TTY
switch.
2024-01-22 16:13:39 +04:00
sodiboo 18566e3366 Watch for canonical filename, not just mtime 2024-01-22 07:42:45 +04:00
Ivan Molodetskikh df48337d83 tty: Delay output config update until resume
We can't do anything while paused.
2024-01-21 10:25:39 +04:00
Ivan Molodetskikh f5e9b40140 tty: Check changes against pending connectors and mode
If we queued some DRM changes, they will be in pending. Also be more
resilient by removing unwrap.
2024-01-21 10:24:42 +04:00
Ivan Molodetskikh 5cacd03e85 Return error instead of broken screenshot for portal 2024-01-21 10:03:13 +04:00
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
44 changed files with 5297 additions and 866 deletions
+21
View File
@@ -0,0 +1,21 @@
---
name: Bug report
about: Report a bug or a crash
title: ''
labels: bug
assignees: ''
---
<!-- Please describe the issue here at the top, then fill in the system information below. -->
### System Information
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
* niri version:
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
* GPU:
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
* CPU:
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: Feature request
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
+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
+464 -204
View File
File diff suppressed because it is too large Load Diff
+20 -12
View File
@@ -1,5 +1,5 @@
[workspace.package]
version = "0.1.0-alpha.3"
version = "0.1.1"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -7,9 +7,11 @@ edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
bitflags = "2.4.1"
bitflags = "2.4.2"
directories = "5.0.1"
serde = { version = "1.0.196", 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"
@@ -37,26 +39,31 @@ 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 }
libc = "0.2.151"
logind-zbus = { version = "3.1.2", optional = true }
libc = "0.2.153"
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.0-alpha.3", path = "niri-config" }
logind-zbus = { version = "3.1.2", optional = true }
niri-config = { version = "0.1.1", path = "niri-config" }
niri-ipc = { version = "0.1.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"
profiling = "1.0.14"
sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.113"
smithay-drm-extras.workspace = true
serde = { version = "1.0.195", features = ["derive"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing.workspace = true
tracy-client = { version = "0.16.5", default-features = false }
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
zbus = { version = "3.14.1", optional = true }
@@ -73,6 +80,7 @@ features = [
"backend_winit",
"desktop",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
@@ -101,7 +109,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~alpha.3"
version = "0.1.1"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+83 -31
View File
@@ -1,35 +1,55 @@
# 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.
Workspaces are dynamic and arranged vertically.
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Features
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor screencasting through xdg-desktop-portal-gnome
- Touchpad gesture to switch workspaces
- Configurable layout: gaps, borders, struts, window sizes
- Live-reloading config
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try.
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
Note that NVIDIA GPUs might have rendering issues.
## Idea
## Inspiration
Niri implements scrollable tiling, heavily inspired by [PaperWM].
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.
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.
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.
## Building
@@ -37,6 +57,8 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
>
> NixOS users, check out https://github.com/sodiboo/niri-flake
>
> For Arch users, there's an AUR package: https://aur.archlinux.org/packages/niri
First, install the dependencies for your distribution.
@@ -46,16 +68,28 @@ 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`.
Next, get latest stable Rust: https://rustup.rs/
Then, build niri with `cargo build --release`.
### NixOS/Nix
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
```
nix run --impure github:guibou/nixGL -- ./results/bin/niri
```
## Installation
@@ -94,10 +128,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 +154,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 +170,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,11 +201,15 @@ Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or
If this fails, it will load [the default configuration file](resources/default-config.kdl).
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
[OBS]: https://flathub.org/apps/com.obsproject.Studio
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
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"
];
};
}
);
}
+1 -1
View File
@@ -9,8 +9,8 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
directories.workspace = true
knuffel = "3.2.0"
miette = "5.10.0"
smithay.workspace = true
tracing.workspace = true
tracy-client.workspace = true
+203 -20
View File
@@ -1,15 +1,15 @@
#[macro_use]
extern crate tracing;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use bitflags::bitflags;
use directories::ProjectDirs;
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 {
@@ -34,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,
@@ -47,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,
@@ -91,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.
@@ -106,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)]
@@ -125,6 +187,8 @@ pub struct Output {
pub name: String,
#[knuffel(child, unwrap(argument), default = 1.)]
pub scale: f64,
#[knuffel(child, unwrap(argument, str), default = Transform::Normal)]
pub transform: Transform,
#[knuffel(child)]
pub position: Option<Position>,
#[knuffel(child, unwrap(argument, str))]
@@ -137,13 +201,63 @@ impl Default for Output {
off: false,
name: String::new(),
scale: 1.,
transform: Transform::Normal,
position: None,
mode: None,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
/// Output transform, which goes counter-clockwise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transform {
Normal,
_90,
_180,
_270,
Flipped,
Flipped90,
Flipped180,
Flipped270,
}
impl FromStr for Transform {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"90" => Ok(Self::_90),
"180" => Ok(Self::_180),
"270" => Ok(Self::_270),
"flipped" => Ok(Self::Flipped),
"flipped-90" => Ok(Self::Flipped90),
"flipped-180" => Ok(Self::Flipped180),
"flipped-270" => Ok(Self::Flipped270),
_ => Err(miette!(concat!(
r#"invalid transform, can be "90", "180", "270", "#,
r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
))),
}
}
}
impl From<Transform> for smithay::utils::Transform {
fn from(value: Transform) -> Self {
match value {
Transform::Normal => Self::Normal,
Transform::_90 => Self::_90,
Transform::_180 => Self::_180,
Transform::_270 => Self::_270,
Transform::Flipped => Self::Flipped,
Transform::Flipped90 => Self::Flipped90,
Transform::Flipped180 => Self::Flipped180,
Transform::Flipped270 => Self::Flipped270,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
#[knuffel(property)]
pub x: i32,
@@ -151,7 +265,7 @@ pub struct Position {
pub y: i32,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mode {
pub width: u16,
pub height: u16,
@@ -168,6 +282,8 @@ pub struct Layout {
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)]
@@ -232,7 +348,8 @@ impl Color {
impl From<Color> for [f32; 4] {
fn from(c: Color) -> Self {
[c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.)
let [r, g, b, a] = [c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.);
[r * a, g * a, b * a, a]
}
}
@@ -274,6 +391,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>);
@@ -345,6 +468,9 @@ pub enum Action {
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[knuffel(argument)] u8),
MoveWorkspaceDown,
MoveWorkspaceUp,
FocusMonitorLeft,
@@ -355,11 +481,20 @@ 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,
MoveWorkspaceToMonitorLeft,
MoveWorkspaceToMonitorRight,
MoveWorkspaceToMonitorDown,
MoveWorkspaceToMonitorUp,
}
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -409,32 +544,23 @@ impl Default for DebugConfig {
}
impl Config {
pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> {
pub fn load(path: &Path) -> miette::Result<Self> {
let _span = tracy_client::span!("Config::load");
Self::load_internal(path).context("error loading config")
}
fn load_internal(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
};
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)
}
}
@@ -571,6 +697,34 @@ 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())))
}
@@ -608,7 +762,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 {
@@ -620,6 +783,7 @@ mod tests {
output "eDP-1" {
scale 2.0
transform "flipped-90"
position x=10 y=20
mode "1920x1080@144"
}
@@ -653,6 +817,8 @@ mod tests {
right 2
top 3
}
center-focused-column "on-overflow"
}
spawn-at-startup "alacritty" "-e" "fish"
@@ -666,6 +832,10 @@ mod tests {
screenshot-path "~/Screenshots/screenshot.png"
hotkey-overlay {
skip-at-startup
}
binds {
Mod+T { spawn "alacritty"; }
Mod+Q { close-window; }
@@ -694,8 +864,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()),
@@ -706,6 +884,7 @@ mod tests {
off: false,
name: "eDP-1".to_owned(),
scale: 2.,
transform: Transform::Flipped90,
position: Some(Position { x: 10, y: 20 }),
mode: Some(Mode {
width: 1920,
@@ -762,6 +941,7 @@ mod tests {
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()],
@@ -772,6 +952,9 @@ mod tests {
xcursor_size: 16,
},
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,
}
+126 -41
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" {
@@ -55,12 +65,16 @@ input {
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
// Transform allows to rotate the output counter-clockwise, valid values are:
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
transform "normal"
// Resolution and, optionally, refresh rate of the output.
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
// 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.
@@ -76,6 +90,14 @@ input {
}
layout {
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decoratins can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
@@ -107,9 +129,9 @@ layout {
// 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.33333
proportion 0.5
proportion 0.667
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
@@ -134,6 +156,14 @@ layout {
// 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.
@@ -161,6 +191,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
@@ -169,34 +205,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.
@@ -210,37 +253,49 @@ 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; }
// ...
// And you can also move a whole workspace to another monitor:
// Mod+Shift+Ctrl+Left { move-workspace-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; }
@@ -251,15 +306,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; }
@@ -301,3 +359,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
}
}
+21 -5
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(),
}
}
@@ -129,13 +138,20 @@ impl Backend {
}
}
pub fn set_monitors_active(&self, active: bool) {
pub fn set_monitors_active(&mut self, active: bool) {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
}
}
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
+377 -171
View File
@@ -20,7 +20,7 @@ use smithay::backend::drm::{
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture};
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -28,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;
@@ -47,6 +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::frame_clock::FrameClock;
use crate::niri::{RedrawState, State};
use crate::render_helpers::AsGlesRenderer;
use crate::utils::get_monotonic_time;
@@ -73,7 +74,10 @@ 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>>>,
// The output config had changed, but the session is paused, so we need to update it on resume.
update_output_config_on_resume: bool,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
}
pub type TtyRenderer<'render, 'alloc> = MultiRenderer<
@@ -140,11 +144,19 @@ pub struct SurfaceDmabufFeedback {
}
impl Tty {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<'static, State>) -> Self {
let (session, notifier) = LibSeatSession::new().unwrap();
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<'static, State>,
) -> anyhow::Result<Self> {
let (session, notifier) = LibSeatSession::new().context(
"Error creating a session. This might mean that you're trying to run niri on a TTY \
that is already busy, for example if you're running this inside tmux that had been \
originally started on a different TTY",
)?;
let seat_name = session.seat();
let udev_backend = UdevBackend::new(session.seat()).unwrap();
let udev_backend =
UdevBackend::new(session.seat()).context("error creating a udev backend")?;
let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
state.backend.tty().on_udev_event(&mut state.niri, event);
});
@@ -153,7 +165,9 @@ impl Tty {
.unwrap();
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
libinput.udev_assign_seat(&seat_name).unwrap();
libinput
.udev_assign_seat(&seat_name)
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
let input_backend = LibinputInputBackend::new(libinput.clone());
event_loop
@@ -188,18 +202,23 @@ impl Tty {
Ok(gles)
};
let api = GbmGlesBackend::with_factory(Box::new(create_renderer));
let gpu_manager = GpuManager::new(api).unwrap();
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
.unwrap_or_else(|| {
let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap();
let primary_node = DrmNode::from_path(primary_gpu_path).unwrap();
.ok_or(())
.or_else(|()| {
let primary_gpu_path = udev::primary_gpu(&seat_name)
.context("error getting the primary GPU")?
.context("couldn't find a GPU")?;
let primary_node = DrmNode::from_path(primary_gpu_path)
.context("error opening the primary GPU DRM node")?;
let primary_render_node = primary_node
.node_with_type(NodeType::Render)
.unwrap()
.unwrap();
(primary_node, primary_render_node)
});
.context("error getting the render node for the primary GPU")?
.context("error getting the render node for the primary GPU")?;
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
})?;
let mut node_path = String::new();
if let Some(path) = primary_render_node.dev_path() {
@@ -209,7 +228,7 @@ impl Tty {
}
info!("using as the render node: {}", node_path);
Self {
Ok(Self {
config,
session,
udev_dispatcher,
@@ -220,8 +239,10 @@ impl Tty {
devices: HashMap::new(),
dmabuf_global: None,
primary_allocator: None,
connectors: Arc::new(Mutex::new(HashMap::new())),
}
update_output_config_on_resume: false,
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn init(&mut self, niri: &mut Niri) {
@@ -274,7 +295,7 @@ impl Tty {
self.libinput.suspend();
for device in self.devices.values() {
for device in self.devices.values_mut() {
device.drm.pause();
}
}
@@ -317,47 +338,13 @@ impl Tty {
device_list.remove(&node.dev_id());
// It hasn't been removed, update its state as usual.
let device = &self.devices[&node];
device.drm.activate();
// HACK: force reset the connectors to make resuming work across sleep.
let device = &self.devices[&node];
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(conn, crtc)| (conn.clone(), crtc))
.collect();
for (conn, crtc) in crtcs {
self.connector_disconnected(niri, node, conn, crtc);
}
let device = self.devices.get_mut(&node).unwrap();
let _ = device.drm_scanner.scan_connectors(&device.drm);
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(conn, crtc)| (conn.clone(), crtc))
.collect();
for (conn, crtc) in crtcs {
if let Err(err) = self.connector_connected(niri, node, conn, crtc) {
warn!("error connecting connector: {err:?}");
}
if let Err(err) = device.drm.activate(true) {
warn!("error activating DRM device: {err:?}");
}
// // Refresh the connectors.
// self.device_changed(node.dev_id(), niri);
// // Refresh the state on unchanged connectors.
// let device = self.devices.get_mut(&node).unwrap();
// for surface in device.surfaces.values_mut() {
// let compositor = &mut surface.compositor;
// if let Err(err) = compositor.surface().reset_state() {
// warn!("error resetting DRM surface state: {err}");
// }
// compositor.reset_buffers();
// }
// niri.queue_redraw_all();
// Refresh the connectors.
self.device_changed(node.dev_id(), niri);
}
// Add new devices.
@@ -366,6 +353,14 @@ impl Tty {
warn!("error adding device: {err:?}");
}
}
if self.update_output_config_on_resume {
self.on_output_config_changed(niri);
}
self.refresh_ipc_outputs();
niri.queue_redraw_all();
}
}
}
@@ -387,15 +382,9 @@ impl Tty {
let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;
let gbm = GbmDevice::new(device_fd)?;
let display = EGLDisplay::new(gbm.clone())?;
let display = unsafe { EGLDisplay::new(gbm.clone())? };
let egl_device = EGLDevice::device_for_display(&display)?;
// HACK: There's an issue in Smithay where the display created by GpuManager will be the
// same as the one we just created here, so when ours is dropped at the end of the scope,
// it will also close the long-lived display in GpuManager. Thus, we need to drop ours
// beforehand.
drop(display);
let render_node = egl_device
.try_get_render_node()?
.context("no render node")?;
@@ -506,12 +495,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 +520,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 +566,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 +600,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 +646,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,13 +719,8 @@ impl Tty {
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
self.connectors
.lock()
.unwrap()
.insert(output_name.clone(), output.clone());
let surface = Surface {
name: output_name,
name: output_name.clone(),
compositor,
dmabuf_feedback,
vblank_frame: None,
@@ -803,36 +732,39 @@ 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)));
self.enabled_outputs
.lock()
.unwrap()
.insert(output_name, output.clone());
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
// Power on all monitors if necessary and queue a redraw on the new one.
niri.event_loop.insert_idle(move |state| {
state.niri.activate_monitors(&state.backend);
state.niri.activate_monitors(&mut state.backend);
state.niri.queue_redraw(output);
});
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 +779,9 @@ impl Tty {
error!("missing output for crtc {crtc:?}");
};
self.connectors.lock().unwrap().remove(&surface.name);
self.enabled_outputs.lock().unwrap().remove(&surface.name);
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
}
fn on_vblank(
@@ -1093,7 +1027,7 @@ impl Tty {
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _, GlesTexture>(&mut renderer, &elements, [0.; 4]) {
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
Ok(res) => {
if self
.config
@@ -1112,7 +1046,7 @@ impl Tty {
niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states);
}
if res.damage.is_some() {
if !res.is_empty {
let presentation_feedbacks =
niri.take_presentation_feedbacks(output, &res.states);
let data = (presentation_feedbacks, target_presentation_time);
@@ -1210,8 +1144,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")]
@@ -1219,13 +1211,166 @@ impl Tty {
self.devices.get(&self.primary_node).map(|d| d.gbm.clone())
}
pub fn set_monitors_active(&self, active: bool) {
for device in self.devices.values() {
for crtc in device.surfaces.keys() {
set_crtc_active(&device.drm, *crtc, active);
pub fn set_monitors_active(&mut self, active: bool) {
// We only disable the CRTC here, this will also reset the
// surface state so that the next call to `render_frame` will
// always produce a new frame and `queue_frame` will change
// the CRTC to active. This makes sure we always enable a CRTC
// within an atomic operation.
if active {
return;
}
for device in self.devices.values_mut() {
for (crtc, surface) in device.surfaces.iter_mut() {
set_crtc_active(&device.drm, *crtc, false);
if let Err(err) = surface.compositor.reset_state() {
warn!("error resetting surface state: {err:?}");
}
}
}
}
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::on_output_config_changed");
// If we're inactive, we can't do anything, so just set a flag for later.
if !self.session.is_active() {
self.update_output_config_on_resume = true;
return;
}
self.update_output_config_on_resume = false;
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 Some(connector) = surface.compositor.pending_connectors().into_iter().next()
else {
error!("surface pending connectors is empty");
continue;
};
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.pending_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 +1545,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))
}
+44 -30
View File
@@ -11,12 +11,11 @@ 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;
use smithay::reexports::winit::window::WindowBuilder;
use smithay::utils::Transform;
use super::RenderResult;
use crate::niri::{RedrawState, State};
@@ -28,24 +27,20 @@ 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 {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = WindowBuilder::new()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.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 (backend, winit) = winit::init_from_builder(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -61,16 +56,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), None, 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 +96,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),
@@ -97,20 +109,18 @@ impl Winit {
WinitEvent::Redraw => state
.niri
.queue_redraw(state.backend.winit().output.clone()),
WinitEvent::CloseRequested => {
state.niri.stop_signal.stop();
state.niri.remove_output(&state.backend.winit().output);
}
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
})
.unwrap();
Self {
Ok(Self {
config,
output,
backend,
damage_tracker,
connectors,
}
ipc_outputs,
enabled_outputs,
})
}
pub fn init(&mut self, niri: &mut Niri) {
@@ -212,7 +222,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()
}
}
+237
View File
@@ -0,0 +1,237 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
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>>>,
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
// notification.
created_path: Option<PathBuf>,
}
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()),
created_path: None,
}
}
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
if self.created_path != created_path {
self.created_path = created_path;
self.buffers.borrow_mut().clear();
}
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
}
pub fn show(&mut self) {
if self.created_path.is_some() {
self.created_path = None;
self.buffers.borrow_mut().clear();
}
// 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() {
let duration = if self.created_path.is_some() {
// Make this quite a bit longer because it comes with a monitor modeset
// (can take a while) and an important hotkeys popup diverting the
// attention.
Duration::from_secs(8)
} else {
Duration::from_secs(4)
};
self.state = State::Shown(target_presentation_time + duration);
}
}
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 path = self.created_path.as_deref();
let mut buffers = self.buffers.borrow_mut();
let buffer = buffers
.entry(scale)
.or_insert_with_key(move |&scale| render(scale, path).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, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("config_error_notification::render");
let padding = PADDING * scale;
let mut text = String::from(TEXT);
let mut border_color = (1., 0.3, 0.3);
if let Some(path) = created_path {
text = format!(
"Created a default config file at \
<span face='monospace' bgcolor='#000000'>{:?}</span>",
path
);
border_color = (0.5, 1., 0.5);
};
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(border_color.0, border_color.1, border_color.2);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(buffer)
}
+5 -12
View File
@@ -8,8 +8,7 @@ use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureBuffer;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
@@ -224,7 +223,7 @@ pub enum RenderCursor {
},
}
type TextureCache = HashMap<(CursorIcon, i32), Vec<TextureBuffer<GlesTexture>>>;
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
#[derive(Default)]
pub struct CursorTextureCache {
@@ -238,12 +237,11 @@ impl CursorTextureCache {
pub fn get(
&self,
renderer: &mut GlesRenderer,
icon: CursorIcon,
scale: i32,
cursor: &XCursor,
idx: usize,
) -> TextureBuffer<GlesTexture> {
) -> MemoryRenderBuffer {
self.cache
.borrow_mut()
.entry((icon, scale))
@@ -252,19 +250,14 @@ impl CursorTextureCache {
.frames()
.iter()
.map(|frame| {
let _span = tracy_client::span!("create TextureBuffer");
TextureBuffer::from_memory(
renderer,
MemoryRenderBuffer::from_slice(
&frame.pixels_rgba,
Fourcc::Abgr8888,
Fourcc::Argb8888,
(frame.width as i32, frame.height as i32),
false,
scale,
Transform::Normal,
None,
)
.unwrap()
})
.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);
}
}
+44 -12
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::{dbus_interface, fdo};
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo, SignalContext};
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 {
@@ -81,12 +112,13 @@ impl DisplayConfig {
Ok((0, monitors, logical_monitors, HashMap::new()))
}
// FIXME: monitors-changed signal.
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
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 }
}
}
+30 -9
View File
@@ -7,23 +7,26 @@ use serde::Deserialize;
use smithay::output::Output;
use smithay::reexports::calloop;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start;
use crate::utils::output_size;
#[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>)>>>,
}
@@ -52,6 +55,13 @@ pub struct Stream {
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
}
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
struct StreamParameters {
/// Size of the stream in logical coordinates.
size: (i32, i32),
}
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
@@ -62,6 +72,7 @@ pub enum ScreenCastToNiri {
StopCast {
session_id: usize,
},
Redraw(Output),
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
@@ -82,7 +93,11 @@ impl ScreenCast {
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
let path = 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 +164,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()));
};
@@ -188,15 +203,21 @@ impl Stream {
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
-> zbus::Result<()>;
#[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters {
let size = output_size(&self.output).into();
StreamParameters { size }
}
}
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 +242,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_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(buffer)
}
+109 -4
View File
@@ -11,8 +11,10 @@ use std::thread;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::input;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -21,7 +23,11 @@ use smithay::utils::{Logical, Rectangle, Size};
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::output::OutputHandler;
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 +44,15 @@ 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::delegate_foreign_toplevel;
use crate::niri::{ClientState, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::utils::output_size;
impl SeatHandler for State {
@@ -70,6 +80,19 @@ impl SeatHandler for State {
set_data_device_focus(dh, seat, client.clone());
set_primary_focus(dh, seat, client);
}
fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: keyboard::LedState) {
let keyboards = self
.niri
.devices
.iter()
.filter(|device| device.has_capability(input::DeviceCapability::Keyboard))
.cloned();
for mut keyboard in keyboards {
keyboard.led_update(led_state.into());
}
}
}
delegate_seat!(State);
delegate_cursor_shape!(State);
@@ -186,6 +209,11 @@ impl DataControlHandler for State {
delegate_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
}
}
delegate_output!(State);
delegate_presentation!(State);
@@ -251,3 +279,80 @@ 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);
impl ForeignToplevelHandler for State {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
&mut self.niri.foreign_toplevel_state
}
fn activate(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
self.niri.layout.activate_window(&window);
self.niri.queue_redraw_all();
}
}
fn close(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
window.toplevel().send_close();
}
}
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((window, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !window
.toplevel()
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
return;
}
let window = window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
}
}
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
}
}
}
delegate_foreign_toplevel!(State);
+135 -33
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;
@@ -11,7 +13,9 @@ use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
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::input_method::InputMethodSeat;
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 +23,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 +94,102 @@ 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) {
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
// a grab. It will likely need refactors in Smithay to support properly since grabs just
// replace each other.
// FIXME: do this properly.
if self.niri.seat.input_method().keyboard_grabbed() {
trace!("ignoring popup grab because IME has keyboard grabbed");
return;
}
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
return;
};
// We need to hand out the grab in a way consistent with what update_keyboard_focus()
// thinks the current focus is, otherwise it will desync and cause weird issues with
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
{
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else {
if layers
.layers_on(Layer::Overlay)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers
.layers_on(Layer::Top)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let Ok(mut grab) = self
.niri
.popups
.grab_popup(root.clone(), popup, seat, serial)
else {
return;
};
let keyboard = seat.get_keyboard().unwrap();
let pointer = seat.get_pointer().unwrap();
let keyboard_grab_mismatches = keyboard.is_grabbed()
&& !(keyboard.has_grab(serial)
|| grab
.previous_serial()
.map_or(true, |s| keyboard.has_grab(s)));
let pointer_grab_mismatches = pointer.is_grabbed()
&& !(pointer.has_grab(serial)
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
if keyboard_grab_mismatches || pointer_grab_mismatches {
grab.ungrab(PopupUngrabStrategy::All);
return;
}
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
@@ -116,9 +214,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
@@ -135,6 +230,13 @@ impl XdgShellHandler for State {
}
self.niri.layout.set_fullscreen(&window, true);
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
if let Some(ws) = self.niri.layout.active_workspace() {
window.toplevel().with_pending_state(|state| {
state.size = Some(ws.view_size());
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
}
}
@@ -151,6 +253,13 @@ impl XdgShellHandler for State {
{
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
} else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
if let Some(ws) = self.niri.layout.active_workspace() {
window.toplevel().with_pending_state(|state| {
state.size = Some(ws.new_window_size());
state.states.unset(xdg_toplevel::State::Fullscreen);
});
}
}
}
@@ -192,49 +301,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 {
zxdg_toplevel_decoration_v1::Mode::ServerSide
} else {
zxdg_toplevel_decoration_v1::Mode::ClientSide
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(mode);
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
}
fn request_mode(
&mut self,
toplevel: ToplevelSurface,
mut mode: zxdg_toplevel_decoration_v1::Mode,
) {
// If prefer-no-csd is unset, then insist on CSD.
if !self.niri.config.borrow().prefer_no_csd {
mode = zxdg_toplevel_decoration_v1::Mode::ClientSide;
}
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 {
zxdg_toplevel_decoration_v1::Mode::ServerSide
} else {
zxdg_toplevel_decoration_v1::Mode::ClientSide
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(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_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(RenderedOverlay {
buffer: Some(buffer),
size: Size::from((width, height)),
scale,
})
}
fn action_name(action: &Action) -> String {
match action {
Action::Quit => String::from("Exit niri"),
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
Action::CloseWindow => String::from("Close Focused Window"),
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
Action::FocusColumnRight => String::from("Focus Column to the Right"),
Action::MoveColumnLeft => String::from("Move Column Left"),
Action::MoveColumnRight => String::from("Move Column Right"),
Action::FocusWorkspaceDown => String::from("Switch Workspace Down"),
Action::FocusWorkspaceUp => String::from("Switch Workspace Up"),
Action::MoveColumnToWorkspaceDown => String::from("Move Column to Workspace Down"),
Action::MoveColumnToWorkspaceUp => String::from("Move Column to Workspace Up"),
Action::MoveWindowToWorkspaceDown => String::from("Move Window to Workspace Down"),
Action::MoveWindowToWorkspaceUp => String::from("Move Window to Workspace Up"),
Action::SwitchPresetColumnWidth => String::from("Switch Preset Column Widths"),
Action::MaximizeColumn => String::from("Maximize Column"),
Action::ConsumeWindowIntoColumn => String::from("Consume Window Into Column"),
Action::ExpelWindowFromColumn => String::from("Expel Window From Column"),
Action::Screenshot => String::from("Take a Screenshot"),
Action::Spawn(args) => format!(
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
args.first().unwrap_or(&String::new())
),
_ => String::from("FIXME: Unknown"),
}
}
fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
let mut name = String::new();
let has_comp_mod = key.modifiers.contains(Modifiers::COMPOSITOR);
if key.modifiers.contains(Modifiers::SUPER)
|| (has_comp_mod && comp_mod == CompositorMod::Super)
{
name.push_str("Super + ");
}
if key.modifiers.contains(Modifiers::ALT) || (has_comp_mod && comp_mod == CompositorMod::Alt) {
name.push_str("Alt + ");
}
if key.modifiers.contains(Modifiers::SHIFT) {
name.push_str("Shift + ");
}
if key.modifiers.contains(Modifiers::CTRL) {
name.push_str("Ctrl + ");
}
name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym)));
name
}
fn prettify_keysym_name(name: &str) -> String {
let name = match name {
"slash" => "/",
"comma" => ",",
"period" => ".",
"minus" => "-",
"equal" => "=",
"grave" => "`",
"Next" => "Page Down",
"Prior" => "Page Up",
"Print" => "PrtSc",
"Return" => "Enter",
_ => name,
};
if name.len() == 1 && name.is_ascii() {
name.to_ascii_uppercase()
} else {
name.into()
}
}
+245 -21
View File
@@ -51,9 +51,19 @@ impl State {
// Power on monitors if they were off.
if should_activate_monitors(&event) {
self.niri.activate_monitors(&self.backend);
self.niri.activate_monitors(&mut 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),
@@ -80,8 +90,21 @@ impl State {
TouchUp { .. } => (),
TouchCancel { .. } => (),
TouchFrame { .. } => (),
SwitchToggle { .. } => (),
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 +112,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 +126,23 @@ impl State {
}
}
}
if device.has_capability(input::DeviceCapability::Keyboard) {
if let Some(led_state) = self
.niri
.seat
.get_keyboard()
.map(|keyboard| keyboard.led_state())
{
device.led_update(led_state.into());
}
}
apply_libinput_settings(&self.niri.config.borrow().input, device);
}
InputEvent::DeviceRemoved { device } => {
self.niri.tablets.remove(device);
self.niri.devices.remove(device);
}
_ => (),
}
@@ -199,6 +229,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 +264,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);
@@ -241,7 +284,7 @@ impl State {
self.niri.suppressed_keys.clear();
}
Action::PowerOffMonitors => {
self.niri.deactivate_monitors(&self.backend);
self.niri.deactivate_monitors(&mut self.backend);
}
Action::ToggleDebugTint => {
self.backend.toggle_debug_tint();
@@ -311,6 +354,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 +409,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 +463,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 +574,65 @@ 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();
}
}
Action::MoveWorkspaceToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
Action::MoveWorkspaceToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
}
}
}
}
@@ -754,12 +880,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();
}
};
@@ -1024,11 +1156,20 @@ impl State {
);
}
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent) {
let res = self
.niri
.layout
.workspace_switch_gesture_update(event.delta_y());
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent)
where
I::Device: 'static,
{
let mut delta_y = event.delta_y();
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if device.config_scroll_natural_scroll_enabled() {
delta_y = -delta_y;
}
}
let res = self.niri.layout.workspace_switch_gesture_update(delta_y);
if let Some(output) = res {
if let Some(output) = output {
self.niri.queue_redraw(output);
@@ -1280,9 +1421,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 +1457,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,6 +1505,61 @@ 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};
+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(())
}
+417 -13
View File
@@ -29,11 +29,12 @@
//! compromise we only keep the first workspace there, and move the rest to the primary output,
//! making the primary output their original output.
use std::cmp::min;
use std::mem;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{self, Config, SizeChange, Struts};
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;
@@ -49,7 +50,7 @@ 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::utils::output_size;
@@ -103,6 +104,11 @@ pub trait LayoutElement: PartialEq {
///
/// This will *not* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_fullscreen(&self) -> bool;
/// Whether we're requesting the element to be fullscreen.
///
/// This *will* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_pending_fullscreen(&self) -> bool;
}
#[derive(Debug)]
@@ -139,6 +145,7 @@ pub struct Options {
struts: Struts,
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.
@@ -152,6 +159,7 @@ impl Default for Options {
struts: Default::default(),
focus_ring: Default::default(),
border: niri_config::default_border(),
center_focused_column: Default::default(),
preset_widths: vec![
ColumnWidth::Proportion(1. / 3.),
ColumnWidth::Proportion(0.5),
@@ -190,6 +198,7 @@ impl Options {
struts: layout.struts,
focus_ring: layout.focus_ring,
border: layout.border,
center_focused_column: layout.center_focused_column,
preset_widths,
default_width,
}
@@ -286,6 +295,11 @@ impl LayoutElement for Window {
.states
.contains(xdg_toplevel::State::Fullscreen)
}
fn is_pending_fullscreen(&self) -> bool {
self.toplevel()
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
}
}
impl<W: LayoutElement> Layout<W> {
@@ -307,11 +321,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.
@@ -325,6 +349,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.
@@ -454,6 +484,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.
@@ -516,6 +569,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);
@@ -611,6 +665,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!()
};
@@ -622,6 +678,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;
@@ -730,6 +787,27 @@ impl<W: LayoutElement> Layout<W> {
mon.workspaces.iter().flat_map(|ws| ws.windows())
}
pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>)) {
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mon.workspaces {
for win in ws.windows() {
f(win, Some(&mon.output));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
for win in ws.windows() {
f(win, None);
}
}
}
}
}
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal {
monitors,
@@ -894,6 +972,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;
@@ -1011,6 +1110,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 {
@@ -1181,6 +1288,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;
@@ -1215,6 +1346,47 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn move_workspace_to_output(&mut self, output: &Output) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
return;
};
let current = &mut monitors[*active_monitor_idx];
if current.active_workspace_idx == current.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(current.output.clone(), current.options.clone());
current.workspaces.push(ws);
}
let mut ws = current.workspaces.remove(current.active_workspace_idx);
current.active_workspace_idx = current.active_workspace_idx.saturating_sub(1);
current.workspace_switch = None;
current.clean_up_workspaces();
ws.set_output(Some(output.clone()));
ws.original_output = OutputId::new(output);
let target_idx = monitors
.iter()
.position(|mon| &mon.output == output)
.unwrap();
let target = &mut monitors[target_idx];
// Insert the workspace after the currently active one. Unless the currently active one is
// the last empty workspace, then insert before.
let target_ws_idx = min(target.active_workspace_idx + 1, target.workspaces.len() - 1);
target.workspaces.insert(target_ws_idx, ws);
target.active_workspace_idx = target_ws_idx;
target.workspace_switch = None;
target.clean_up_workspaces();
*active_monitor_idx = target_idx;
}
pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
@@ -1300,7 +1472,7 @@ impl<W: LayoutElement> Layout<W> {
for monitor in monitors {
if let Some(WorkspaceSwitch::Gesture(gesture)) = &mut monitor.workspace_switch {
// Normalize like GNOME Shell's workspace switching.
let delta_y = -delta_y / 400.;
let delta_y = delta_y / 400.;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(monitor.workspaces.len() - 1) as f64;
@@ -1424,6 +1596,7 @@ mod tests {
requested_size: Cell<Option<Size<i32, Logical>>>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
pending_fullscreen: Cell<bool>,
}
#[derive(Debug, Clone)]
@@ -1443,6 +1616,7 @@ mod tests {
requested_size: Cell::new(None),
min_size,
max_size,
pending_fullscreen: Cell::new(false),
}))
}
@@ -1499,9 +1673,12 @@ mod tests {
fn request_size(&self, size: Size<i32, Logical>) {
self.0.requested_size.set(Some(size));
self.0.pending_fullscreen.set(false);
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.0.pending_fullscreen.set(true);
}
fn min_size(&self) -> Size<i32, Logical> {
self.0.min_size
@@ -1528,6 +1705,10 @@ mod tests {
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.0.pending_fullscreen.get()
}
}
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
@@ -1605,14 +1786,19 @@ 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),
SetWindowHeight(#[proptest(strategy = "arbitrary_size_change()")] SizeChange),
Communicate(#[proptest(strategy = "1..=5usize")] usize),
MoveWorkspaceToOutput(#[proptest(strategy = "1..=5u8")] u8),
}
impl Op {
@@ -1726,6 +1912,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 {
@@ -1734,6 +1923,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(),
@@ -1775,6 +1972,14 @@ mod tests {
layout.update_window(&win);
}
}
Op::MoveWorkspaceToOutput(id) => {
let name = format!("output{id}");
let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else {
return;
};
layout.move_workspace_to_output(&output);
}
}
}
}
@@ -1831,6 +2036,9 @@ mod tests {
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FullscreenWindow(1),
Op::FullscreenWindow(2),
Op::FullscreenWindow(3),
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusWindowUp,
@@ -1852,10 +2060,16 @@ 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,
Op::MoveWindowUpOrToWorkspaceUp,
Op::MoveWorkspaceToOutput(1),
];
for third in every_op {
@@ -1953,6 +2167,9 @@ mod tests {
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FullscreenWindow(1),
Op::FullscreenWindow(2),
Op::FullscreenWindow(3),
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusWindowUp,
@@ -1974,6 +2191,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,
@@ -2187,8 +2409,196 @@ 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);
}
#[test]
fn move_workspace_to_output() {
let ops = [
Op::AddOutput(1),
Op::AddOutput(2),
Op::FocusOutput(1),
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
},
Op::MoveWorkspaceToOutput(2),
];
let mut layout = Layout::default();
for op in ops {
op.apply(&mut layout);
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = layout.monitor_set
else {
unreachable!()
};
assert_eq!(active_monitor_idx, 1);
assert_eq!(monitors[0].workspaces.len(), 1);
assert!(!monitors[0].workspaces[0].has_windows());
assert_eq!(monitors[1].active_workspace_idx, 0);
assert_eq!(monitors[1].workspaces.len(), 2);
assert!(monitors[1].workspaces[0].has_windows());
}
#[test]
fn fullscreen() {
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::FullscreenWindow(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! {
@@ -2203,13 +2613,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);
}
+77 -2
View File
@@ -12,7 +12,7 @@ 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;
@@ -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));
}
+194 -50
View File
@@ -3,7 +3,7 @@ use std::iter::zip;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{PresetWidth, SizeChange, Struts};
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};
@@ -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,19 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn view_size(&self) -> Size<i32, Logical> {
self.view_size
}
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 {
@@ -342,7 +355,7 @@ impl<W: LayoutElement> Workspace<W> {
))
}
pub fn configure_new_window(&self, window: &Window) {
pub fn new_window_size(&self) -> Size<i32, Logical> {
let width = if let Some(width) = self.options.default_width {
let mut width = width.resolve(&self.options, self.working_area.size.w);
if !self.options.border.off {
@@ -358,12 +371,15 @@ impl<W: LayoutElement> Workspace<W> {
height -= self.options.border.width as i32 * 2;
}
let size = Size::from((width, max(height, 1)));
Size::from((width, max(height, 1)))
}
pub fn configure_new_window(&self, window: &Window) {
let size = self.new_window_size();
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 +413,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 +440,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 +567,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 +671,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 +732,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 +819,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 +880,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 +905,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 {
@@ -1025,8 +1158,14 @@ impl<W: LayoutElement> Column<W> {
options,
};
let is_pending_fullscreen = window.is_pending_fullscreen();
rv.add_window(window);
if is_pending_fullscreen {
rv.set_fullscreen(true);
}
rv
}
@@ -1315,6 +1454,10 @@ impl<W: LayoutElement> Column<W> {
if self.is_fullscreen {
assert_eq!(self.tiles.len(), 1);
}
for tile in &self.tiles {
assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen());
}
}
fn toggle_width(&mut self) {
@@ -1509,7 +1652,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);
+123 -16
View File
@@ -3,14 +3,19 @@ extern crate tracing;
mod animation;
mod backend;
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 protocols;
mod render_helpers;
mod screenshot_ui;
mod utils;
@@ -22,11 +27,14 @@ mod dummy_pw_utils;
mod pw_utils;
use std::ffi::OsString;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use clap::{Parser, Subcommand};
use directories::ProjectDirs;
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
use git_version::git_version;
@@ -40,10 +48,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")]
@@ -67,6 +76,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)]
pub enum Msg {
/// List connected outputs.
Outputs,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -114,27 +139,76 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
Config::load(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) {
Ok((config, path)) => (config, Some(path)),
Err(err) => {
warn!("{err:?}");
(Config::default(), None)
let mut config_created = false;
let path = cli.config.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
if let Err(err) = fs::create_dir_all(default_parent) {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
return Some(default_path);
}
};
// Create the config and fill it with the default config if it doesn't exist.
let new_file = File::options()
.read(true)
.write(true)
.create_new(true)
.open(&default_path);
match new_file {
Ok(mut new_file) => {
let default = include_bytes!("../resources/default-config.kdl");
match new_file.write_all(default) {
Ok(()) => {
config_created = true;
info!("wrote default config to {:?}", &default_path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &default_path)
}
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
}
Some(default_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);
@@ -146,7 +220,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
event_loop.handle(),
event_loop.get_signal(),
display,
);
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = &state.niri.socket_name;
@@ -156,6 +231,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();
@@ -178,7 +259,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// Set up config file watcher.
let _watcher = if let Some(path) = path {
let _watcher = if let Some(path) = path.clone() {
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(path.clone(), tx);
event_loop
@@ -200,6 +281,13 @@ 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();
} else if config_created {
state.niri.config_error_notification.show_created(path);
}
// Run the compositor.
event_loop
.run(None, &mut state, |state| state.refresh_and_flush_clients())
@@ -208,6 +296,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([
@@ -235,3 +331,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)
}
+622 -160
View File
File diff suppressed because it is too large Load Diff
+466
View File
@@ -0,0 +1,466 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use arrayvec::ArrayVec;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
};
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
const VERSION: u32 = 3;
pub struct ForeignToplevelManagerState {
display: DisplayHandle,
instances: Vec<ZwlrForeignToplevelManagerV1>,
toplevels: HashMap<WlSurface, ToplevelData>,
}
pub trait ForeignToplevelHandler {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
fn activate(&mut self, wl_surface: WlSurface);
fn close(&mut self, wl_surface: WlSurface);
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
fn unset_fullscreen(&mut self, wl_surface: WlSurface);
}
struct ToplevelData {
title: Option<String>,
app_id: Option<String>,
states: ArrayVec<u32, 3>,
output: Option<Output>,
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
// FIXME: parent.
}
pub struct ForeignToplevelGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ForeignToplevelManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ForeignToplevelGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
Self {
display: display.clone(),
instances: Vec::new(),
toplevels: HashMap::new(),
}
}
}
pub fn refresh(state: &mut State) {
let _span = tracy_client::span!("foreign_toplevel::refresh");
let protocol_state = &mut state.niri.foreign_toplevel_state;
// Handle closed windows.
protocol_state.toplevels.retain(|surface, data| {
if state.niri.layout.find_window_and_output(surface).is_some() {
return true;
}
for instance in data.instances.keys() {
instance.closed();
}
false
});
// Handle new and existing windows.
//
// Save the focused window for last, this way when the focus changes, we will first deactivate
// the previous window and only then activate the newly focused window.
let mut focused = None;
state.niri.layout.with_windows(|window, output| {
let wl_surface = window.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
if state.niri.keyboard_focus.as_ref() == Some(wl_surface) {
focused = Some((window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
let wl_surface = window.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
});
}
}
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
let Some(client) = wl_output.client() else {
return;
};
let protocol_state = &mut state.niri.foreign_toplevel_state;
for data in protocol_state.toplevels.values_mut() {
if data.output.as_ref() != Some(output) {
continue;
}
for (instance, outputs) in &mut data.instances {
if instance.client().as_ref() != Some(&client) {
continue;
}
instance.output_enter(wl_output);
instance.done();
outputs.push(wl_output.clone());
}
}
}
fn refresh_toplevel(
protocol_state: &mut ForeignToplevelManagerState,
wl_surface: &WlSurface,
role: &XdgToplevelSurfaceRoleAttributes,
output: Option<&Output>,
has_focus: bool,
) {
let states = to_state_vec(&role.current.states, has_focus);
match protocol_state.toplevels.entry(wl_surface.clone()) {
Entry::Occupied(entry) => {
// Existing window, check if anything changed.
let data = entry.into_mut();
let mut new_title = None;
if data.title != role.title {
data.title = role.title.clone();
new_title = role.title.as_deref();
if new_title.is_none() {
error!("toplevel title changed to None");
}
}
let mut new_app_id = None;
if data.app_id != role.app_id {
data.app_id = role.app_id.clone();
new_app_id = role.app_id.as_deref();
if new_app_id.is_none() {
error!("toplevel app_id changed to None");
}
}
let mut states_changed = false;
if data.states != states {
data.states = states;
states_changed = true;
}
let mut output_changed = false;
if data.output.as_ref() != output {
data.output = output.cloned();
output_changed = true;
}
let something_changed =
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
if something_changed {
for (instance, outputs) in &mut data.instances {
if let Some(new_title) = new_title {
instance.title(new_title.to_owned());
}
if let Some(new_app_id) = new_app_id {
instance.app_id(new_app_id.to_owned());
}
if states_changed {
instance.state(data.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
}
if output_changed {
for wl_output in outputs.drain(..) {
instance.output_leave(&wl_output);
}
if let Some(output) = &data.output {
if let Some(client) = instance.client() {
for wl_output in output.client_outputs(&client) {
instance.output_enter(&wl_output);
outputs.push(wl_output);
}
}
}
}
instance.done();
}
}
for outputs in data.instances.values_mut() {
// Clean up dead wl_outputs.
outputs.retain(|x| x.is_alive());
}
}
Entry::Vacant(entry) => {
// New window, start tracking it.
let mut data = ToplevelData {
title: role.title.clone(),
app_id: role.app_id.clone(),
states,
output: output.cloned(),
instances: HashMap::new(),
};
for manager in &protocol_state.instances {
if let Some(client) = manager.client() {
data.add_instance::<State>(&protocol_state.display, &client, manager);
}
}
entry.insert(data);
}
}
}
impl ToplevelData {
fn add_instance<D>(
&mut self,
handle: &DisplayHandle,
client: &Client,
manager: &ZwlrForeignToplevelManagerV1,
) where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: 'static,
{
let toplevel = client
.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
.unwrap();
manager.toplevel(&toplevel);
if let Some(title) = &self.title {
toplevel.title(title.clone());
}
if let Some(app_id) = &self.app_id {
toplevel.app_id(app_id.clone());
}
toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
let mut outputs = Vec::new();
if let Some(output) = &self.output {
for wl_output in output.client_outputs(client) {
toplevel.output_enter(&wl_output);
outputs.push(wl_output);
}
}
toplevel.done();
self.instances.insert(toplevel, outputs);
}
}
impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
for ForeignToplevelManagerState
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn bind(
state: &mut D,
handle: &DisplayHandle,
client: &Client,
resource: New<ZwlrForeignToplevelManagerV1>,
_global_data: &ForeignToplevelGlobalData,
data_init: &mut DataInit<'_, D>,
) {
let manager = data_init.init(resource, ());
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.add_instance::<D>(handle, client, &manager);
}
state.instances.push(manager);
}
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelManagerV1,
request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
resource.finished();
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelManagerV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
}
impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelHandleV1,
request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let protocol_state = state.foreign_toplevel_manager_state();
let Some((surface, _)) = protocol_state
.toplevels
.iter()
.find(|(_, data)| data.instances.contains_key(resource))
else {
return;
};
let surface = surface.clone();
match request {
zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
state.activate(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::Close => {
state.close(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
state.set_fullscreen(surface, output);
}
zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
state.unset_fullscreen(surface);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelHandleV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.instances.retain(|instance, _| instance != resource);
}
}
}
fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3> {
let mut rv = ArrayVec::new();
if states.contains(xdg_toplevel::State::Maximized) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
}
if states.contains(xdg_toplevel::State::Fullscreen) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
}
// HACK: wlr-foreign-toplevel-management states:
//
// These have the same meaning as the states with the same names defined in xdg-toplevel
//
// However, clients such as sfwbar and fcitx seem to treat the activated state as keyboard
// focus, i.e. they don't expect multiple windows to have it set at once. Even Waybar which
// handles multiple activated windows correctly uses it in its design in such a way that
// keyboard focus would make more sense. Let's do what the clients expect.
if has_focus {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
}
rv
}
#[macro_export]
macro_rules! delegate_foreign_toplevel {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
};
}
+1
View File
@@ -0,0 +1 @@
pub mod foreign_toplevel;
+16 -1
View File
@@ -27,6 +27,7 @@ use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Size};
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
@@ -43,6 +44,7 @@ pub struct Cast {
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub output: Output,
pub size: Size<i32, Physical>,
pub cursor_mode: CursorMode,
pub last_frame_time: Duration,
pub min_time_between_frames: Rc<Cell<Duration>>,
@@ -95,14 +97,25 @@ 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;
let transform = output.current_transform();
let size = transform.transform_size(size);
let refresh = mode.refresh;
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
@@ -158,6 +171,7 @@ impl PipeWire {
StreamState::Connecting => (),
StreamState::Streaming => {
is_active.set(true);
redraw();
}
}
}
@@ -373,6 +387,7 @@ impl PipeWire {
_listener: listener,
is_active,
output,
size,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
+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,
{
+12 -5
View File
@@ -41,6 +41,8 @@ pub enum ScreenshotUi {
pub struct OutputData {
size: Size<i32, Physical>,
scale: i32,
transform: Transform,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
buffers: [SolidColorBuffer; 8],
@@ -93,6 +95,7 @@ impl ScreenshotUi {
)
}
};
let scale = selection.0.current_scale().integer_scale();
let selection = (
selection.0,
@@ -103,13 +106,14 @@ impl ScreenshotUi {
let output_data = screenshots
.into_iter()
.map(|(output, texture)| {
let output_transform = output.current_transform();
let transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
let size = 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 +130,8 @@ impl ScreenshotUi {
let locations = [Default::default(); 8];
let data = OutputData {
size,
scale,
transform,
texture,
texture_buffer,
buffers,
@@ -330,9 +336,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, Transform)> {
if let Self::Open { output_data, .. } = self {
Some(output_data.get(output)?.size)
let data = output_data.get(output)?;
Some((data.size, data.scale, data.transform))
} else {
None
}
+7
View File
@@ -190,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;
}
+18 -4
View File
@@ -27,7 +27,18 @@ impl Watcher {
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
// this "should" be as simple as mtime, but it does not quite work in practice;
// it doesn't work if the config is a symlink, and its target changes but the
// new target and old target have identical mtimes.
//
// in practice, this does not occur on any systems other than nix.
// because, on nix practically everything is a symlink to /nix/store
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
// so, symlink targets change frequently when mtime doesn't.
let mut last_props = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
.ok();
loop {
thread::sleep(Duration::from_millis(500));
@@ -36,8 +47,11 @@ impl Watcher {
break;
}
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
if last_mtime != Some(mtime) {
if let Ok(new_props) = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
{
if last_props.as_ref() != Some(&new_props) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
@@ -45,7 +59,7 @@ impl Watcher {
break;
}
last_mtime = Some(mtime);
last_props = Some(new_props);
}
}
}