Compare commits

..

135 Commits

Author SHA1 Message Date
Ivan Molodetskikh 7ff2de19b9 wiki: Update block-out-from-screencast img 2024-03-30 13:25:54 +04:00
Ivan Molodetskikh f81b51f4c0 Bump version to 0.1.4 2024-03-30 11:39:12 +04:00
Ivan Molodetskikh a90221d924 Fix crash when stopping screencast session twice 2024-03-30 10:50:02 +04:00
Ivan Molodetskikh ab22816521 wiki: Fix default config link 2024-03-29 14:23:28 +04:00
Ivan Molodetskikh 56a55f1ad1 Improve README 2024-03-29 14:20:51 +04:00
Ivan Molodetskikh f7fde74a8d input: Add Tracy span to notify activity 2024-03-29 14:13:08 +04:00
Ivan Molodetskikh 0470a833a1 Move IPC into wiki 2024-03-29 14:13:01 +04:00
Ivan Molodetskikh 092420ec5a tty: Try to proceed when can't get render node
This is a workaround that should make split display/render devices work.
2024-03-29 09:09:33 +04:00
Ivan Molodetskikh f46e937949 wiki: Improvements 2024-03-28 21:07:48 +04:00
Ivan Molodetskikh c9a47f8283 wiki: Mention creating screenshot directory 2024-03-28 21:01:26 +04:00
Ivan Molodetskikh 9b7ed57d37 Create screenshot directory if it doesn't exist 2024-03-28 20:59:42 +04:00
Ivan Molodetskikh cf409a4ea6 wiki: Link all sections from the overview 2024-03-28 20:53:26 +04:00
Ivan Molodetskikh 83bd2317ee wiki: Add miscellaneous 2024-03-28 20:53:15 +04:00
Ivan Molodetskikh 0f19003611 default-config: Link layout wiki 2024-03-28 20:48:27 +04:00
Ivan Molodetskikh 470d65a060 wiki: Improve Layout 2024-03-28 17:50:34 +04:00
Ivan Molodetskikh 4f421907cd wiki: Add Layout 2024-03-28 17:35:36 +04:00
Ivan Molodetskikh b4eaaed19e Upgrade dependencies 2024-03-28 17:35:27 +04:00
Ivan Molodetskikh d3d178fac7 wiki: Mention niri msg focused-window 2024-03-28 13:47:27 +04:00
Ivan Molodetskikh 3091102365 Implement niri msg focused-window 2024-03-28 13:45:24 +04:00
Ivan Molodetskikh a7b3819214 tty: Add check for zero gamma size 2024-03-28 07:47:57 +04:00
Ivan Molodetskikh 1eff5aeb75 wiki: Add one more bind example 2024-03-27 21:48:19 +04:00
Ivan Molodetskikh 9f0566b1ab wiki: Fix em-dash 2024-03-27 21:47:08 +04:00
Ivan Molodetskikh 3c75082df2 wiki: Add key bindings 2024-03-27 21:46:11 +04:00
Ivan Molodetskikh 9927c15f68 Replace config transform with ipc 2024-03-27 17:03:17 +04:00
Ivan Molodetskikh cf87a185a9 Add logical output info and preferred modes to IPC 2024-03-27 14:54:24 +04:00
Ivan Molodetskikh e276c906bf Expose more info in DisplayConfig impl
Needed for the new xdp-gnome.
2024-03-27 09:46:18 +04:00
Ivan Molodetskikh 571768af43 Make ipc_outputs Arc Mutex 2024-03-27 08:27:14 +04:00
Ivan Molodetskikh c09d5eb048 wiki: Clarify refresh rate in outputs 2024-03-26 21:29:47 +04:00
Ivan Molodetskikh 1a3e31a5cf wiki: Fix order in outputs 2024-03-26 21:28:09 +04:00
Ivan Molodetskikh 62f14d42dc wiki: Add outputs section 2024-03-26 21:26:43 +04:00
Ivan Molodetskikh ce644852d2 wiki: Fix wording in Animations 2024-03-26 21:02:07 +04:00
Ivan Molodetskikh ffe9a03b58 wiki: Remove anchored link from debug page
The github wiki action rewriter can't deal with them.
2024-03-26 20:01:37 +04:00
Ivan Molodetskikh 3c84de5215 wiki: Document debug options 2024-03-26 19:53:58 +04:00
Ivan Molodetskikh cd555bbad7 wiki: Clarify config breaking change between releases 2024-03-26 19:40:23 +04:00
Ivan Molodetskikh 287d9b6b3f wiki: Clarify breaking change policy 2024-03-26 18:47:16 +04:00
Ivan Molodetskikh 9bd812c37a wiki: Add config breaking change policy 2024-03-26 18:44:42 +04:00
Ivan Molodetskikh 0845eef326 wiki: Add easing example 2024-03-26 18:35:48 +04:00
Ivan Molodetskikh 4d8cb3a6e3 wiki: Add animations page 2024-03-26 18:33:53 +04:00
Ivan Molodetskikh 48b009ba63 CI: Depend on a later commit of the github wiki action 2024-03-26 17:32:06 +04:00
Ivan Molodetskikh addd1f5267 wiki: Replace links with relative 2024-03-26 17:24:17 +04:00
Ivan Molodetskikh b30f8fb2cc wiki: Use relative links in sidebar 2024-03-26 17:21:40 +04:00
Ivan Molodetskikh f5c97faf4a wiki: Improve window rules formatting 2024-03-26 13:35:37 +04:00
Ivan Molodetskikh 8f1bbea863 wiki: Expand fullscreen rule example 2024-03-26 13:25:01 +04:00
Ivan Molodetskikh 5e7eafb2fd wiki: Add is-focused example 2024-03-26 13:21:20 +04:00
Ivan Molodetskikh 41b13aa881 wiki: Expand window rules 2024-03-26 13:18:30 +04:00
Ivan Molodetskikh fd7f2287f0 wiki: Clarify Overview 2024-03-26 13:18:18 +04:00
Ivan Molodetskikh 1635337504 wiki: Clarify screenshot UI blocking out 2024-03-26 11:25:09 +04:00
Ivan Molodetskikh b677592f11 wiki: Mention inability to unset rule 2024-03-26 11:17:40 +04:00
Ivan Molodetskikh ad2795bb27 wiki: Clarify window rule matching example 2024-03-26 11:11:26 +04:00
Ivan Molodetskikh 7826003a81 wiki: Sets->Set for consistency 2024-03-26 11:05:47 +04:00
Ivan Molodetskikh 768fbea14d CI: Download LFS files in publish-wiki 2024-03-26 10:55:10 +04:00
Ivan Molodetskikh e46003f91f default-config: Delete some input and window rule settings
Replace them with links to the wiki.
2024-03-26 10:49:49 +04:00
Ivan Molodetskikh 5360ddb320 wiki: Fix missing backtick 2024-03-26 10:44:23 +04:00
Ivan Molodetskikh d4b271fead wiki: Document window rules 2024-03-26 10:40:19 +04:00
Ivan Molodetskikh de6685f3ab wiki: Add missing commas 2024-03-26 08:21:58 +04:00
Ivan Molodetskikh 662e2df0e1 wiki: Clarify auto back and forth like in flake docs 2024-03-26 08:19:18 +04:00
Ivan Molodetskikh 26c4824047 wiki: Input and more Overview 2024-03-26 08:12:55 +04:00
Ivan Molodetskikh 78dbb2308e wiki: Start writing input configuration 2024-03-25 22:51:21 +04:00
Ivan Molodetskikh 1dce99352e wiki: Fix links 2024-03-25 21:56:37 +04:00
Ivan Molodetskikh 0b6d62f65e wiki: Add configuration overview 2024-03-25 21:50:21 +04:00
Ivan Molodetskikh cf54f75113 Move wiki into the main repository 2024-03-25 21:16:03 +04:00
Ivan Molodetskikh 0d90876ad8 CI: Disable checkout progress from nix 2024-03-25 21:13:23 +04:00
Ivan Molodetskikh e5bd1113ba default-config: Make example use screen-capture blocking 2024-03-24 11:42:27 +04:00
Ivan Molodetskikh 6f765db44e default-config: Clarify interactivity in block-out-from "screen-capture" 2024-03-24 11:37:07 +04:00
Ivan Molodetskikh 5f23d344d5 Make screenshot UI render target-aware 2024-03-24 11:25:48 +04:00
Ivan Molodetskikh e43e10f44e Remove unnecessary reference 2024-03-24 11:11:00 +04:00
Ivan Molodetskikh 493c8dc890 Implement block-out-from window rule, fix alpha on window screenshots 2024-03-24 10:22:56 +04:00
Ivan Molodetskikh 8b4a9d68e0 Implement opacity window rule 2024-03-24 08:30:26 +04:00
Ivan Molodetskikh a16a0f0e52 Implement TouchpadScroll binds 2024-03-23 20:30:45 +04:00
Ivan Molodetskikh 6ba195211b Rename WheelTracker to ScrollTracker 2024-03-23 20:17:01 +04:00
Ivan Molodetskikh afaaf36f27 Avoid scroll bind lookup until it is triggered 2024-03-23 19:20:44 +04:00
Ivan Molodetskikh f1b36b0dce Send pending configure after recomputing window rules 2024-03-23 18:57:06 +04:00
Ivan Molodetskikh 6ec65bc0d6 Add is-focused window rule matcher 2024-03-23 16:16:52 +04:00
Ivan Molodetskikh d65446421f Make rules non-pub 2024-03-23 16:04:35 +04:00
Ivan Molodetskikh 24078cfea2 Make need_to_recompute_rules non-pub 2024-03-23 16:02:23 +04:00
Ivan Molodetskikh 5cc2c31a5b Split State::refresh() to get a trace span 2024-03-23 15:45:44 +04:00
Ivan Molodetskikh b7ed2fb82a Add is-active window rule matcher 2024-03-23 15:45:44 +04:00
Ivan Molodetskikh f3f02aca20 Lift output clones from queue_redraw() 2024-03-23 15:45:44 +04:00
Ivan Molodetskikh 021a2a1af7 Don't use an idle for queued redraw tracking
This way we can order the redraw after all the refreshing, where it
should be.
2024-03-23 15:45:44 +04:00
Ivan Molodetskikh 354f0b039a Pass Un/Mapped to window rule resolution 2024-03-23 15:45:44 +04:00
Andreas Stührk d120e0c451 input: Add support for ISO level3 shift modifier
This modifier is typically called "AltGr" on keyboards or "Mod5" in xkb
layouts. Requires a Smithay update.
2024-03-23 15:45:27 +04:00
Ivan Molodetskikh 0f724f2011 Stop hardcoding "us" default layout
XKB has its own way to pick the default.
2024-03-23 10:10:01 +04:00
Ivan Molodetskikh 46131c87a5 default-config: Clarify that wheel binds are affected by natural-scroll 2024-03-23 09:02:50 +04:00
Ivan Molodetskikh c66319314e Fix vertical wheel binds on winit 2024-03-23 09:00:55 +04:00
Ivan Molodetskikh b09dbb80c7 [cfg-breaking] Rename Wheel* to WheelScroll* bindings
Less confusion, and clearer that they are affected by natural-scroll.
2024-03-23 08:49:58 +04:00
Ivan Molodetskikh 54e6a01284 Allow clippy false positive harder 2024-03-22 21:24:11 +04:00
Ivan Molodetskikh 7721e3fc44 Allow clippy false positive 2024-03-22 21:14:03 +04:00
Ivan Molodetskikh 0d2fdb49ef default-config: Add mouse wheel binds 2024-03-22 20:56:20 +04:00
Ivan Molodetskikh b06e51da60 Implement bind cooldown-ms 2024-03-22 20:47:40 +04:00
Ivan Molodetskikh 6c08ba307a input: Make functions return the whole bind 2024-03-22 20:47:35 +04:00
Ivan Molodetskikh 4b2fdd0776 Implement mouse wheel bindings 2024-03-22 13:10:40 +04:00
Ivan Molodetskikh 969519b5d8 input: Generalize bound_action() to Trigger 2024-03-22 11:11:45 +04:00
Ivan Molodetskikh a0c8c39b06 Make binds accept wheel names 2024-03-22 10:36:19 +04:00
Ivan Molodetskikh 977f1487c2 input: Fix discrete axis value on winit 2024-03-22 09:41:10 +04:00
Ivan Molodetskikh fbe021fbdf input: Rename discrete => v120 2024-03-22 09:35:17 +04:00
Ivan Molodetskikh db49deb7fd Implement draw-border-with-background window rule 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh c61361de3c Implement window rule reloading and min/max size rules 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh 3963f537a4 Wrap mapped windows in a Mapped 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh f31e105043 Make window a subdirectory 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh bbb4caeb8c Remove remaining Window-specific functions 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh d421e1fbf8 Move PartialEq from LayoutElement to an associated type 2024-03-19 18:29:13 +04:00
FluxTape 23ac3d7323 Workspace back and forth (#253)
* implement workspace back and forth

* Make our own ID counter instead of SerialCounter, use a newtype

* Rename FocusWorkspaceBackAndForth to FocusWorkspacePrevious

* Add focus-workspace-previous to tests

* Don't special case in switch_workspace_previous

* Minor clean up

* Add switch_workspace_auto_back_and_forth to tests

* Skip animation on switch_workspace_previous

* Preserve previous_workspace_id on workspace movement

* Make Workspace::id private with a getter

Reduce the chance it gets overwritten.

* Add test for workspace ID uniqueness

* Update previous workspace ID upon moving workspace across monitors

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-03-19 07:27:52 -07:00
Ivan Molodetskikh c3327d36da tty: Generalize DRM property helpers 2024-03-19 09:00:00 +04:00
Ivan Molodetskikh e0da101c73 Disable screencast when PipeWire is missing
This can cause a panic.
2024-03-19 08:59:28 +04:00
Ivan Molodetskikh 4740682904 README: Move configuration up 2024-03-18 19:36:18 +04:00
Ivan Molodetskikh df9d721f74 Implement focus-follows-mouse 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh d970abead8 Keep track of output and window in PointerFocus separately 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh 4f6ed9dfc9 Fix lock surface pointer location 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh 84302796dc Take workspace switch gesture into account for visual rect 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh a39e703fc3 Don't warp if currently using tablet
The tablet will override the position anyway.
2024-03-18 19:31:11 +04:00
Ivan Molodetskikh a55db6c6c4 Warp mouse to focus on window closing 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh a011b385d8 Warp mouse to focus on new window appearing 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh 2984722f80 Warp mouse only if layout is focused 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh 118773e17d Track keyboard focus component 2024-03-18 19:31:11 +04:00
FluxTape 741bee461c Implement warp-mouse-to-focus 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh 0c57815fbf Restore gamma on TTY switch back 2024-03-15 22:02:29 +04:00
Ivan Molodetskikh cf89c789c3 README: Link touchpad gestures to showcase video 2024-03-15 09:56:11 -07:00
Ivan Molodetskikh 642c6e7512 Store gamma changes to apply on session resume 2024-03-15 13:29:36 +04:00
Ivan Molodetskikh 6839a118bb Implement gamma adjustment via GAMMA_LUT property 2024-03-15 13:29:36 +04:00
Ivan Molodetskikh 9ae3cad82b gamma-control: Misc. clean ups and fixes 2024-03-15 13:29:36 +04:00
phuhl 89dfaa6cac Adds support for wlr_gamma_control_unstable_v1 protocol 2024-03-15 13:29:36 +04:00
Ivan Molodetskikh f6ffe8b3ab tty: Make binding EGL wl-display optional 2024-03-14 18:08:52 +04:00
la .uetcis cc83ff008d Add clickfinger in touchpad config (#256)
* Add clickfinger in touchpad config

* Change `clickfinger` to `click-method`

* Change `bottom_areas` to `button_areas`

* Change button_areas to button-areas

For consistency.

* Reorder click methods in error message

The most usual one comes first.

* default-config: Move click-method down

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-03-13 21:26:03 -07:00
Ivan Molodetskikh ba4e7481c3 default-config: Clarify how to power on monitors 2024-03-14 08:04:34 +04:00
Ivan Molodetskikh c15bc2a028 tty: Set max bpc to 8 2024-03-13 09:15:18 +04:00
Ivan Molodetskikh bf1cc98886 Update Smithay 2024-03-13 07:17:19 +04:00
Ivan Molodetskikh 5f137b77d3 Reapply "Add wp-viewporter"
This reverts commit 40cec34aa4.

The Chromium issues are now fixed.
2024-03-12 17:22:53 +04:00
Ivan Molodetskikh 128d573e74 Update Smithay (viewporter fixes) 2024-03-12 17:22:52 +04:00
Ivan Molodetskikh ed8a6afe80 Add a 1 Hz fallback frame callback timer
gamescope + Minecraft with NeoForge throws an error upon starting if
there are no frame callbacks, thus making it the first client that has a
problem. Also, apparently, Veloren disconnects from server with VSync
and no frame callbacks.
2024-03-12 10:42:09 +04:00
Ivan Molodetskikh 43aa2f95be Fix new clone_from Clippy lints 2024-03-12 10:42:09 +04:00
Ivan Molodetskikh 5c0a1f4d6f Fix spelling mistake 2024-03-12 10:42:09 +04:00
Ivan Molodetskikh 8c46611c29 Preserve view offset for activate_prev_column_on_removal 2024-03-10 17:59:10 +04:00
Ivan Molodetskikh 40cec34aa4 Revert "Add wp-viewporter"
This reverts commit 348690afb6.

Apparently this breaks input in Chromium: the input region won't resize
together with the window.
2024-03-10 17:59:10 +04:00
Ivan Molodetskikh 1971a41fdd utils/spawning: Pass grandchild PID only on systemd
libc::close_range() is not available on musl, so do this workaround for
now.
2024-03-09 18:37:11 +04:00
Ivan Molodetskikh 4ea90140d4 Fix warning on --no-default-features 2024-03-09 18:36:01 +04:00
75 changed files with 6344 additions and 1717 deletions
+1
View File
@@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text
+15
View File
@@ -183,6 +183,8 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check flake inputs
uses: DeterminateSystems/flake-checker-action@v4
@@ -194,3 +196,16 @@ jobs:
- run: nix build
continue-on-error: true
publish-wiki:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
lfs: true
show-progress: false
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
Generated
+200 -171
View File
File diff suppressed because it is too large Load Diff
+11 -9
View File
@@ -2,7 +2,7 @@
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.3"
version = "0.1.4"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -10,8 +10,8 @@ edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
anyhow = "1.0.80"
bitflags = "2.4.2"
anyhow = "1.0.81"
bitflags = "2.5.0"
clap = { version = "~4.4.18", features = ["derive"] }
serde = { version = "1.0.197", features = ["derive"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
@@ -44,19 +44,21 @@ anyhow.workspace = true
arrayvec = "0.7.4"
async-channel = { version = "2.2.0", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.2"
bitflags.workspace = true
bytemuck = { version = "1.15.0", features = ["derive"] }
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
drm-ffi = "0.7.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.25.0"
glam = "0.27.0"
input = { version = "0.9.0", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.153"
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.3", path = "niri-config" }
niri-ipc = { version = "0.1.3", path = "niri-ipc", features = ["clap"] }
niri-config = { version = "0.1.4", path = "niri-config" }
niri-ipc = { version = "0.1.4", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.19.2"
pipewire = { version = "0.8.0", optional = true }
@@ -65,7 +67,7 @@ portable-atomic = { version = "1.6.0", default-features = false, features = ["fl
profiling = "1.0.15"
sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.114"
serde_json = "1.0.115"
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
@@ -120,7 +122,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.3"
version = "0.1.4"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+6 -23
View File
@@ -28,7 +28,8 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor screencasting through xdg-desktop-portal-gnome
- Touchpad gestures
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
- [Touchpad gestures](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515)
- Configurable layout: gaps, borders, struts, window sizes
- Live-reloading config
@@ -85,7 +86,7 @@ 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:
```
@@ -131,20 +132,10 @@ 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
## Configuration
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.
Please check [this wiki page](https://github.com/YaLTeR/niri/wiki/Configuration:-Overview) for an overview of niri configuration.
It also links to wiki pages containing thorough documentation for all options with examples.
## Default Hotkeys
@@ -195,14 +186,6 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
## Configuration
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
If this fails, it will load [the default configuration file](resources/default-config.kdl).
Please use the default configuration file as the starting point for your custom configuration.
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
+2 -2
View File
@@ -12,8 +12,8 @@ bitflags.workspace = true
csscolorparser = "0.6.2"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.3", path = "../niri-ipc" }
regex = "1.10.3"
niri-ipc = { version = "0.1.4", path = "../niri-ipc" }
regex = "1.10.4"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
+237 -76
View File
@@ -5,11 +5,12 @@ use std::collections::HashSet;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use bitflags::bitflags;
use knuffel::errors::DecodeError;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{LayoutSwitchTarget, SizeChange};
use niri_ipc::{LayoutSwitchTarget, SizeChange, Transform};
use regex::Regex;
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
@@ -69,6 +70,12 @@ pub struct Input {
pub touch: Touch,
#[knuffel(child)]
pub disable_power_key_handling: bool,
#[knuffel(child)]
pub warp_mouse_to_focus: bool,
#[knuffel(child)]
pub focus_follows_mouse: bool,
#[knuffel(child)]
pub workspace_auto_back_and_forth: bool,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
@@ -90,8 +97,8 @@ pub struct Xkb {
pub rules: String,
#[knuffel(child, unwrap(argument), default)]
pub model: String,
#[knuffel(child, unwrap(argument))]
pub layout: Option<String>,
#[knuffel(child, unwrap(argument), default)]
pub layout: String,
#[knuffel(child, unwrap(argument), default)]
pub variant: String,
#[knuffel(child, unwrap(argument))]
@@ -103,7 +110,7 @@ impl Xkb {
XkbConfig {
rules: &self.rules,
model: &self.model,
layout: self.layout.as_deref().unwrap_or("us"),
layout: &self.layout,
variant: &self.variant,
options: self.options.clone(),
}
@@ -142,6 +149,8 @@ pub struct Touchpad {
pub dwtp: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument, str))]
pub click_method: Option<ClickMethod>,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: f64,
#[knuffel(child, unwrap(argument, str))]
@@ -170,6 +179,21 @@ pub struct Trackpoint {
pub accel_profile: Option<AccelProfile>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClickMethod {
Clickfinger,
ButtonAreas,
}
impl From<ClickMethod> for input::ClickMethod {
fn from(value: ClickMethod) -> Self {
match value {
ClickMethod::Clickfinger => Self::Clickfinger,
ClickMethod::ButtonAreas => Self::ButtonAreas,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccelProfile {
Adaptive,
@@ -241,55 +265,6 @@ impl Default for Output {
}
}
/// 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)]
@@ -651,6 +626,7 @@ pub struct WindowRule {
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,
// Rules applied at initial configure.
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
#[knuffel(child, unwrap(argument))]
@@ -659,38 +635,82 @@ pub struct WindowRule {
pub open_maximized: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_fullscreen: Option<bool>,
// Rules applied dynamically.
#[knuffel(child, unwrap(argument))]
pub min_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub min_height: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_height: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub draw_border_with_background: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
}
// Remember to update the PartialEq impl when adding fields!
#[derive(knuffel::Decode, Debug, Default, Clone)]
pub struct Match {
#[knuffel(property, str)]
pub app_id: Option<Regex>,
#[knuffel(property, str)]
pub title: Option<Regex>,
#[knuffel(property)]
pub is_active: Option<bool>,
#[knuffel(property)]
pub is_focused: Option<bool>,
}
impl PartialEq for Match {
fn eq(&self, other: &Self) -> bool {
self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str)
self.is_active == other.is_active
&& self.is_focused == other.is_focused
&& self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str)
&& self.title.as_ref().map(Regex::as_str) == other.title.as_ref().map(Regex::as_str)
}
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockOutFrom {
Screencast,
ScreenCapture,
}
#[derive(Debug, Default, PartialEq)]
pub struct Binds(pub Vec<Bind>);
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Bind {
pub key: Key,
pub action: Action,
pub cooldown: Option<Duration>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub struct Key {
pub keysym: Keysym,
pub trigger: Trigger,
pub modifiers: Modifiers,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Trigger {
Keysym(Keysym),
WheelScrollDown,
WheelScrollUp,
WheelScrollLeft,
WheelScrollRight,
TouchpadScrollDown,
TouchpadScrollUp,
TouchpadScrollLeft,
TouchpadScrollRight,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Modifiers : u8 {
@@ -698,7 +718,8 @@ bitflags! {
const SHIFT = 2;
const ALT = 4;
const SUPER = 8;
const COMPOSITOR = 16;
const ISO_LEVEL3_SHIFT = 16;
const COMPOSITOR = 32;
}
}
@@ -745,6 +766,7 @@ pub enum Action {
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[knuffel(argument)] u8),
FocusWorkspacePrevious,
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
@@ -814,6 +836,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::FocusWorkspaceDown => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { index } => Self::FocusWorkspace(index),
niri_ipc::Action::FocusWorkspacePrevious => Self::FocusWorkspacePrevious,
niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown,
niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp,
niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index),
@@ -851,6 +874,8 @@ impl From<niri_ipc::Action> for Action {
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct DebugConfig {
#[knuffel(child, unwrap(argument))]
pub preview_render: Option<PreviewRender>,
#[knuffel(child)]
pub dbus_interfaces_in_non_session_instances: bool,
#[knuffel(child)]
@@ -867,6 +892,12 @@ pub struct DebugConfig {
pub emulate_zero_presentation_time: bool,
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreviewRender {
Screencast,
ScreenCapture,
}
impl Config {
pub fn load(path: &Path) -> miette::Result<Self> {
let _span = tracy_client::span!("Config::load");
@@ -1357,13 +1388,45 @@ where
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
expect_only_children(node, ctx);
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
for val in node.arguments.iter() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"no arguments expected for this node",
))
}
let key = node
.node_name
.parse::<Key>()
.map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?;
let mut cooldown = None;
for (name, val) in &node.properties {
match &***name {
"cooldown-ms" => {
cooldown = Some(Duration::from_millis(
knuffel::traits::DecodeScalar::decode(val, ctx)?,
));
}
name_str => {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name_str.escape_default()),
));
}
}
}
let mut children = node.children();
// If the action is invalid but the key is fine, we still want to return something.
@@ -1372,6 +1435,7 @@ where
let dummy = Self {
key,
action: Action::Spawn(vec![]),
cooldown: None,
};
if let Some(child) = children.next() {
@@ -1383,7 +1447,11 @@ where
));
}
match Action::decode_node(child, ctx) {
Ok(action) => Ok(Self { key, action }),
Ok(action) => Ok(Self {
key,
action,
cooldown,
}),
Err(e) => {
ctx.emit_error(e);
Ok(dummy)
@@ -1455,17 +1523,54 @@ impl FromStr for Key {
modifiers |= Modifiers::ALT;
} else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") {
modifiers |= Modifiers::SUPER;
} else if part.eq_ignore_ascii_case("iso_level3_shift")
|| part.eq_ignore_ascii_case("mod5")
{
modifiers |= Modifiers::ISO_LEVEL3_SHIFT;
} else {
return Err(miette!("invalid modifier: {part}"));
}
}
let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
if keysym.raw() == KEY_NoSymbol {
return Err(miette!("invalid key: {key}"));
}
let trigger = if key.eq_ignore_ascii_case("WheelScrollDown") {
Trigger::WheelScrollDown
} else if key.eq_ignore_ascii_case("WheelScrollUp") {
Trigger::WheelScrollUp
} else if key.eq_ignore_ascii_case("WheelScrollLeft") {
Trigger::WheelScrollLeft
} else if key.eq_ignore_ascii_case("WheelScrollRight") {
Trigger::WheelScrollRight
} else if key.eq_ignore_ascii_case("TouchpadScrollDown") {
Trigger::TouchpadScrollDown
} else if key.eq_ignore_ascii_case("TouchpadScrollUp") {
Trigger::TouchpadScrollUp
} else if key.eq_ignore_ascii_case("TouchpadScrollLeft") {
Trigger::TouchpadScrollLeft
} else if key.eq_ignore_ascii_case("TouchpadScrollRight") {
Trigger::TouchpadScrollRight
} else {
let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
if keysym.raw() == KEY_NoSymbol {
return Err(miette!("invalid key: {key}"));
}
Trigger::Keysym(keysym)
};
Ok(Key { keysym, modifiers })
Ok(Key { trigger, modifiers })
}
}
impl FromStr for ClickMethod {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"clickfinger" => Ok(Self::Clickfinger),
"button-areas" => Ok(Self::ButtonAreas),
_ => Err(miette!(
r#"invalid click method, can be "button-areas" or "clickfinger""#
)),
}
}
}
@@ -1534,6 +1639,7 @@ mod tests {
tap
dwt
dwtp
click-method "clickfinger"
accel-speed 0.2
accel-profile "flat"
tap-button-map "left-middle-right"
@@ -1560,6 +1666,10 @@ mod tests {
}
disable-power-key-handling
warp-mouse-to-focus
focus-follows-mouse
workspace-auto-back-and-forth
}
output "eDP-1" {
@@ -1640,6 +1750,7 @@ mod tests {
window-rule {
match app-id=".*alacritty"
exclude title="~"
exclude is-active=true is-focused=false
open-on-output "eDP-1"
open-maximized true
@@ -1654,6 +1765,7 @@ mod tests {
Mod+Comma { consume-window-into-column; }
Mod+1 { focus-workspace 1; }
Mod+Shift+E { quit skip-confirmation=true; }
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
}
debug {
@@ -1664,7 +1776,7 @@ mod tests {
input: Input {
keyboard: Keyboard {
xkb: Xkb {
layout: Some("us,ru".to_owned()),
layout: "us,ru".to_owned(),
options: Some("grp:win_space_toggle".to_owned()),
..Default::default()
},
@@ -1676,6 +1788,7 @@ mod tests {
tap: true,
dwt: true,
dwtp: true,
click_method: Some(ClickMethod::Clickfinger),
natural_scroll: false,
accel_speed: 0.2,
accel_profile: Some(AccelProfile::Flat),
@@ -1698,6 +1811,9 @@ mod tests {
map_to_output: Some("eDP-1".to_owned()),
},
disable_power_key_handling: true,
warp_mouse_to_focus: true,
focus_follows_mouse: true,
workspace_auto_back_and_forth: true,
},
outputs: vec![Output {
off: false,
@@ -1820,11 +1936,23 @@ mod tests {
matches: vec![Match {
app_id: Some(Regex::new(".*alacritty").unwrap()),
title: None,
is_active: None,
is_focused: None,
}],
excludes: vec![Match {
app_id: None,
title: Some(Regex::new("~").unwrap()),
}],
excludes: vec![
Match {
app_id: None,
title: Some(Regex::new("~").unwrap()),
is_active: None,
is_focused: None,
},
Match {
app_id: None,
title: None,
is_active: Some(true),
is_focused: Some(false),
},
],
open_on_output: Some("eDP-1".to_owned()),
open_maximized: Some(true),
open_fullscreen: Some(false),
@@ -1833,52 +1961,67 @@ mod tests {
binds: Binds(vec![
Bind {
key: Key {
keysym: Keysym::t,
trigger: Trigger::Keysym(Keysym::t),
modifiers: Modifiers::COMPOSITOR,
},
action: Action::Spawn(vec!["alacritty".to_owned()]),
cooldown: None,
},
Bind {
key: Key {
keysym: Keysym::q,
trigger: Trigger::Keysym(Keysym::q),
modifiers: Modifiers::COMPOSITOR,
},
action: Action::CloseWindow,
cooldown: None,
},
Bind {
key: Key {
keysym: Keysym::h,
trigger: Trigger::Keysym(Keysym::h),
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
action: Action::FocusMonitorLeft,
cooldown: None,
},
Bind {
key: Key {
keysym: Keysym::l,
trigger: Trigger::Keysym(Keysym::l),
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL,
},
action: Action::MoveWindowToMonitorRight,
cooldown: None,
},
Bind {
key: Key {
keysym: Keysym::comma,
trigger: Trigger::Keysym(Keysym::comma),
modifiers: Modifiers::COMPOSITOR,
},
action: Action::ConsumeWindowIntoColumn,
cooldown: None,
},
Bind {
key: Key {
keysym: Keysym::_1,
trigger: Trigger::Keysym(Keysym::_1),
modifiers: Modifiers::COMPOSITOR,
},
action: Action::FocusWorkspace(1),
cooldown: None,
},
Bind {
key: Key {
keysym: Keysym::e,
trigger: Trigger::Keysym(Keysym::e),
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
action: Action::Quit(true),
cooldown: None,
},
Bind {
key: Key {
trigger: Trigger::WheelScrollDown,
modifiers: Modifiers::COMPOSITOR,
},
action: Action::FocusWorkspaceDown,
cooldown: Some(Duration::from_millis(150)),
},
]),
debug: DebugConfig {
@@ -1950,4 +2093,22 @@ mod tests {
assert!("-".parse::<SizeChange>().is_err());
assert!("10% ".parse::<SizeChange>().is_err());
}
#[test]
fn parse_iso_level3_shift() {
assert_eq!(
"ISO_Level3_Shift+A".parse::<Key>().unwrap(),
Key {
trigger: Trigger::Keysym(Keysym::a),
modifiers: Modifiers::ISO_LEVEL3_SHIFT
},
);
assert_eq!(
"Mod5+A".parse::<Key>().unwrap(),
Key {
trigger: Trigger::Keysym(Keysym::a),
modifiers: Modifiers::ISO_LEVEL3_SHIFT
},
);
}
}
+83
View File
@@ -14,6 +14,8 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
pub enum Request {
/// Request information about connected outputs.
Outputs,
/// Request information about the focused window.
FocusedWindow,
/// Perform an action.
Action(Action),
}
@@ -37,6 +39,8 @@ pub enum Response {
///
/// Map from connector name to output info.
Outputs(HashMap<String, Output>),
/// Information about the focused window.
FocusedWindow(Option<Window>),
}
/// Actions that niri can perform.
@@ -123,6 +127,8 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Focus the previous workspace.
FocusWorkspacePrevious,
/// Move the focused window to the workspace below.
MoveWindowToWorkspaceDown,
/// Move the focused window to the workspace above.
@@ -246,6 +252,10 @@ pub struct Output {
///
/// `None` if the output is disabled.
pub current_mode: Option<usize>,
/// Logical output information.
///
/// `None` if the output is not mapped to any logical output (for example, if it is disabled).
pub logical: Option<LogicalOutput>,
}
/// Output mode.
@@ -257,6 +267,58 @@ pub struct Mode {
pub height: u16,
/// Refresh rate in millihertz.
pub refresh_rate: u32,
/// Whether this mode is preferred by the monitor.
pub is_preferred: bool,
}
/// Logical output in the compositor's coordinate space.
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct LogicalOutput {
/// Logical X position.
pub x: i32,
/// Logical Y position.
pub y: i32,
/// Width in logical pixels.
pub width: u32,
/// Height in logical pixels.
pub height: u32,
/// Scale factor.
pub scale: f64,
/// Transform.
pub transform: Transform,
}
/// Output transform, which goes counter-clockwise.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transform {
/// Untransformed.
Normal,
/// Rotated by 90°.
#[serde(rename = "90")]
_90,
/// Rotated by 180°.
#[serde(rename = "180")]
_180,
/// Rotated by 270°.
#[serde(rename = "270")]
_270,
/// Flipped horizontally.
Flipped,
/// Rotated by 90° and flipped horizontally.
Flipped90,
/// Flipped vertically.
Flipped180,
/// Rotated by 270° and flipped horizontally.
Flipped270,
}
/// Toplevel window.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Window {
/// Title, if set.
pub title: Option<String>,
/// Application ID, if set.
pub app_id: Option<String>,
}
impl FromStr for SizeChange {
@@ -310,3 +372,24 @@ impl FromStr for LayoutSwitchTarget {
}
}
}
impl FromStr for Transform {
type Err = &'static str;
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(concat!(
r#"invalid transform, can be "90", "180", "270", "#,
r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
)),
}
}
}
+2 -2
View File
@@ -11,8 +11,8 @@ repository.workspace = true
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
niri = { version = "0.1.3", path = ".." }
niri-config = { version = "0.1.3", path = "../niri-config" }
niri = { version = "0.1.4", path = ".." }
niri-config = { version = "0.1.4", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+13 -12
View File
@@ -2,7 +2,8 @@ use std::collections::HashMap;
use std::time::Duration;
use niri::layout::workspace::ColumnWidth;
use niri::layout::Options;
use niri::layout::{LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri::utils::get_monotonic_time;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
@@ -73,12 +74,12 @@ impl Layout {
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
rv.layout.activate_window(&rv.windows[0]);
rv.layout.activate_window(&0);
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
rv
@@ -91,7 +92,7 @@ impl Layout {
rv.add_step(delay, move |l| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -105,7 +106,7 @@ impl Layout {
rv.add_step(delay, move |l| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
}
@@ -122,7 +123,7 @@ impl Layout {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
rv
@@ -138,7 +139,7 @@ impl Layout {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
rv
@@ -147,7 +148,7 @@ impl Layout {
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
self.layout.add_window(window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
self.layout.update_window(window.id());
}
self.windows.push(window);
}
@@ -159,9 +160,9 @@ impl Layout {
width: Option<ColumnWidth>,
) {
self.layout
.add_window_right_of(right_of, window.clone(), width, false);
.add_window_right_of(right_of.id(), window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
self.layout.update_window(window.id());
}
self.windows.push(window);
}
@@ -183,7 +184,7 @@ impl TestCase for Layout {
self.layout.update_output_size(&self.output);
for win in &self.windows {
if win.communicate() {
self.layout.update_window(win);
self.layout.update_window(win.id());
}
}
}
@@ -222,7 +223,7 @@ impl TestCase for Layout {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer)
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
+2
View File
@@ -2,6 +2,7 @@ use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -110,6 +111,7 @@ impl TestCase for Tile {
Scale::from(1.),
size.to_logical(1),
true,
RenderTarget::Output,
)
.map(|elem| Box::new(elem) as _)
.collect()
+8 -1
View File
@@ -1,4 +1,5 @@
use niri::layout::LayoutElement;
use niri::render_helpers::RenderTarget;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Scale, Size};
@@ -49,7 +50,13 @@ impl TestCase for Window {
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
self.window
.render(renderer, location, Scale::from(1.))
.render(
renderer,
location,
Scale::from(1.),
1.,
RenderTarget::Output,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
+54 -33
View File
@@ -4,6 +4,8 @@ use std::rc::Rc;
use niri::layout::{LayoutElement, LayoutElementRenderElement};
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::RenderTarget;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::Output;
@@ -12,7 +14,6 @@ use smithay::utils::{Logical, Point, Scale, Size, Transform};
#[derive(Debug)]
struct TestWindowInner {
id: usize,
size: Size<i32, Logical>,
requested_size: Option<Size<i32, Logical>>,
min_size: Size<i32, Logical>,
@@ -24,7 +25,10 @@ struct TestWindowInner {
}
#[derive(Debug, Clone)]
pub struct TestWindow(Rc<RefCell<TestWindowInner>>);
pub struct TestWindow {
id: usize,
inner: Rc<RefCell<TestWindowInner>>,
}
impl TestWindow {
pub fn freeform(id: usize) -> Self {
@@ -33,17 +37,19 @@ impl TestWindow {
let max_size = Size::from((0, 0));
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
Self(Rc::new(RefCell::new(TestWindowInner {
Self {
id,
size,
requested_size: None,
min_size,
max_size,
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
})))
inner: Rc::new(RefCell::new(TestWindowInner {
size,
requested_size: None,
min_size,
max_size,
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
})),
}
}
pub fn fixed_size(id: usize) -> Self {
@@ -56,24 +62,24 @@ impl TestWindow {
}
pub fn set_min_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().min_size = size;
self.inner.borrow_mut().min_size = size;
}
pub fn set_max_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().max_size = size;
self.inner.borrow_mut().max_size = size;
}
pub fn set_color(&self, color: [f32; 4]) {
self.0.borrow_mut().buffer.set_color(color);
self.inner.borrow_mut().buffer.set_color(color);
}
pub fn set_csd_shadow_width(&self, width: i32) {
self.0.borrow_mut().csd_shadow_width = width;
self.inner.borrow_mut().csd_shadow_width = width;
}
pub fn communicate(&self) -> bool {
let mut rv = false;
let mut inner = self.0.borrow_mut();
let mut inner = self.inner.borrow_mut();
let mut new_size = inner.size;
@@ -117,15 +123,15 @@ impl TestWindow {
}
}
impl PartialEq for TestWindow {
fn eq(&self, other: &Self) -> bool {
self.0.borrow().id == other.0.borrow().id
}
}
impl LayoutElement for TestWindow {
type Id = usize;
fn id(&self) -> &Self::Id {
&self.id
}
fn size(&self) -> Size<i32, Logical> {
self.0.borrow().size
self.inner.borrow().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
@@ -141,15 +147,17 @@ impl LayoutElement for TestWindow {
_renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
_target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
let inner = self.0.borrow();
let inner = self.inner.borrow();
vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location.to_physical_precise_round(scale),
scale,
1.,
alpha,
Kind::Unspecified,
)
.into(),
@@ -158,7 +166,7 @@ impl LayoutElement for TestWindow {
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
.to_physical_precise_round(scale),
scale,
1.,
alpha,
Kind::Unspecified,
)
.into(),
@@ -166,20 +174,20 @@ impl LayoutElement for TestWindow {
}
fn request_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().requested_size = Some(size);
self.0.borrow_mut().pending_fullscreen = false;
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.0.borrow_mut().pending_fullscreen = true;
self.inner.borrow_mut().pending_fullscreen = true;
}
fn min_size(&self) -> Size<i32, Logical> {
self.0.borrow().min_size
self.inner.borrow().min_size
}
fn max_size(&self) -> Size<i32, Logical> {
self.0.borrow().max_size
self.inner.borrow().max_size
}
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
@@ -198,11 +206,24 @@ impl LayoutElement for TestWindow {
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_activated(&mut self, _active: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn send_pending_configure(&self) {}
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.0.borrow().pending_fullscreen
self.inner.borrow().pending_fullscreen
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
}
+109 -251
View File
@@ -1,6 +1,11 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
// Input device configuration.
// Find the full list of options on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
@@ -11,16 +16,6 @@ input {
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
}
// You can set the keyboard repeat parameters. The defaults match wlroots and sway.
// Delay is in milliseconds before the repeat starts. Rate is in characters per second.
// repeat-delay 600
// repeat-rate 25
// Niri can remember the keyboard layout globally (the default) or per-window.
// - "global" - layout change is global for all windows.
// - "window" - layout is tracked for each window individually.
// track-layout "global"
}
// Next sections include libinput settings.
@@ -32,7 +27,6 @@ input {
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// tap-button-map "left-middle-right"
}
mouse {
@@ -41,48 +35,23 @@ input {
// accel-profile "flat"
}
trackpoint {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
tablet {
// Set the name of the output (see below) which the tablet will map to.
// If this is unset or the output doesn't exist, the tablet maps to one of the
// existing outputs.
map-to-output "eDP-1"
}
touch {
// Set the name of the output (see below) which touch input will map to.
// If this is unset or the output doesn't exist, touch input maps to one of the
// existing outputs.
map-to-output "eDP-1"
}
// By default, niri will take over the power button to make it sleep
// instead of power off.
// Uncomment this if you would like to configure the power button elsewhere
// (i.e. logind.conf).
// disable-power-key-handling
// Focus windows and outputs automatically when moving the mouse into them.
// focus-follows-mouse
}
// 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".
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
// Uncomment this line to disable this output.
// off
// Scale is a floating-point number, but at the moment only integer values work.
scale 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
@@ -91,19 +60,58 @@ input {
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
mode "1920x1080@120.030"
// 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"
// Position of the output in the global coordinate space.
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
// The cursor can only move between directly adjacent outputs.
// Output scale has to be taken into account for positioning:
// Output scale and rotation has to be taken into account for positioning:
// outputs are sized in logical, or scaled, pixels.
// For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080,
// so to put another output directly adjacent to it on the right, set its x to 1920.
// It the position is unset or results in an overlap, the output is instead placed
// If the position is unset or results in an overlap, the output is instead placed
// automatically.
position x=1280 y=0
}
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
gaps 16
// 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.
// - "always", the focused column will always be centered.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
center-focused-column "never"
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// 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 decorations can have an arbitrary shape.
@@ -111,6 +119,9 @@ layout {
// 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.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
// You can change how the focus ring looks.
focus-ring {
@@ -131,9 +142,6 @@ layout {
// Color of the ring on inactive monitors.
inactive-color "#505050"
// Additionally, there's a legacy RGBA syntax:
// active-color 127 200 255 255
// You can also use gradients. They take precedence over solid colors.
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
// The angle is the same as in linear-gradient, and is optional,
@@ -163,27 +171,6 @@ layout {
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// Set gaps around windows in logical pixels.
gaps 16
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
@@ -195,14 +182,6 @@ 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.
@@ -210,22 +189,6 @@ layout {
// which may be more convenient to use.
// spawn-at-startup "alacritty" "-e" "fish"
// You can override environment variables for processes spawned by niri.
environment {
// Set a variable like this:
// QT_QPA_PLATFORM "wayland"
// Remove a variable by using null as the value:
// DISPLAY null
}
cursor {
// Change the theme and size of the cursor as well as set the
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
// xcursor-theme "default"
// xcursor-size 24
}
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
@@ -239,145 +202,43 @@ 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
}
// Animation settings.
// The wiki explains how to configure individual animations:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations
animations {
// Uncomment to turn off all animations.
// off
// Slow down all animations by this factor. Values below 1 speed them up instead.
// slowdown 3.0
// You can configure all individual animations.
// Available settings are the same for all of them.
// - off disables the animation.
//
// Niri supports two animation types: easing and spring.
// You can set properties for only ONE of them.
//
// Easing has the following settings:
// - duration-ms sets the duration of the animation in milliseconds.
// - curve sets the easing curve. Currently, available curves
// are "ease-out-cubic" and "ease-out-expo".
//
// Spring animations work better with touchpad gestures, because they
// take into account the velocity of your fingers as you release the swipe.
// The parameters are less obvious and generally should be tuned
// with trial and error. Notably, you cannot directly set the duration.
// You can use this app to help visualize how the spring parameters
// change the animation: https://flathub.org/apps/app.drey.Elastic
//
// A spring animation is configured like this:
// - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
//
// The damping ratio goes from 0.1 to 10.0 and has the following properties:
// - below 1.0: underdamped spring, will oscillate in the end.
// - above 1.0: overdamped spring, won't oscillate.
// - 1.0: critically damped spring, comes to rest in minimum possible time
// without oscillations.
//
// However, even with damping ratio = 1.0 the spring animation may oscillate
// if "launched" with enough velocity from a touchpad swipe.
//
// Lower stiffness will result in a slower animation more prone to oscillation.
//
// Set epsilon to a lower value if the animation "jumps" in the end.
//
// The spring mass is hardcoded to 1.0 and cannot be changed. Instead, change
// stiffness proportionally. E.g. increasing mass by 2x is the same as
// decreasing stiffness by 2x.
// Animation when switching workspaces up and down,
// including after the touchpad gesture.
workspace-switch {
// off
// spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
// All horizontal camera view movement:
// - When a window off-screen is focused and the camera scrolls to it.
// - When a new window appears off-screen and the camera scrolls to it.
// - When a window resizes bigger and the camera scrolls to show it in full.
// - And so on.
horizontal-view-movement {
// off
// spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
// Window opening animation. Note that this one has different defaults.
window-open {
// off
// duration-ms 150
// curve "ease-out-expo"
// Example for a slightly bouncy window opening:
// spring damping-ratio=0.8 stiffness=1000 epsilon=0.0001
}
// Config parse error and new default config creation notification
// open/close animation.
config-notification-open-close {
// off
// spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
}
}
// Window rules let you adjust behavior for individual windows.
// They are processed in order of appearance in this file.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
// Match directives control which windows this rule will apply to.
// You can match by app-id and by title.
// The window must match all properties of the match directive.
match app-id="org.myapp.MyApp" title="My Cool App"
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// There can be multiple match directives. A window must match any one
// of the rule's match directives.
//
// If there are no match directives, any window will match the rule.
match title="Second App"
// You can also add exclude directives which have the same properties.
// If a window matches any exclude directive, it won't match this rule.
//
// Both app-id and title are regular expressions.
// Raw KDL strings are helpful here.
exclude app-id=r#"\.unwanted\."#
// Here are the properties that you can set on a window rule.
// You can override the default column width.
default-column-width { proportion 0.75; }
// You can set the output that this window will initially open on.
// If such an output does not exist, it will open on the currently
// focused output as usual.
open-on-output "eDP-1"
// Make this window open as a maximized column.
open-maximized true
// Make this window open fullscreen.
open-fullscreen true
// You can also set this to false to prevent a window from opening fullscreen.
// open-fullscreen false
}
// Here's a useful example. Work around WezTerm's initial configure bug
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
// The regular expression can match anywhere in the string.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
// Example: block out two password managers from screen capture.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
match app-id=r#"^org\.gnome\.World\.Secrets$"#
block-out-from "screen-capture"
// Use this instead if you want them visible on third-party screenshot tools.
// block-out-from "screencast"
}
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
@@ -481,6 +342,38 @@ binds {
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// You can bind mouse wheel scroll ticks using the following syntax.
// These binds will change direction based on the natural-scroll setting.
//
// To avoid scrolling through workspaces really fast, you can use
// the cooldown-ms property. The bind will be rate-limited to this value.
// You can set a cooldown on any bind, but it's most useful for the wheel.
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
// Usually scrolling up and down with Shift in applications results in
// horizontal scrolling; these binds replicate that.
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// Similarly, you can bind touchpad scroll "ticks".
// Touchpad scrolling is continuous, so for these binds it is split into
// discrete intervals.
// These binds are also affected by touchpad's natural-scroll, so these
// example binds are "inverted", since we have natural-scroll enabled for
// touchpads by default.
// Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; }
// Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; }
// You can refer to workspaces by index. However, keep in mind that
// niri is a dynamic workspace system, so these commands are kind of
// "best effort". Trying to refer to a workspace index bigger than
@@ -511,6 +404,9 @@ binds {
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+1 { move-window-to-workspace 1; }
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
Mod+Comma { consume-window-into-column; }
Mod+Period { expel-window-from-column; }
@@ -551,47 +447,9 @@ binds {
Alt+Print { screenshot-window; }
// The quit action will show a confirmation dialog to avoid accidental exits.
// If you want to skip the confirmation dialog, set the flag like so:
// Mod+Shift+E { quit skip-confirmation=true; }
Mod+Shift+E { quit; }
// Powers off the monitors. To turn them back on, do any input like
// moving the mouse or pressing any other key.
Mod+Shift+P { power-off-monitors; }
// This debug bind will tint all surfaces green, unless they are being
// directly scanned out. It's therefore useful to check if direct scanout
// is working.
// 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
// Override the DRM device that niri will use for all rendering.
// render-drm-device "/dev/dri/renderD129"
// Enable the color-transformations capability of the Smithay renderer.
// May cause a slight decrease in rendering performance.
// enable-color-transformations-capability
// Emulate zero (unknown) presentation time returned from DRM.
// This is a thing on NVIDIA proprietary drivers, so this flag can be
// used to test that we don't break too hard on those systems.
// emulate-zero-presentation-time
}
+3 -11
View File
@@ -1,6 +1,4 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -33,6 +31,8 @@ pub enum RenderResult {
Skipped,
}
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
impl Backend {
pub fn init(&mut self, niri: &mut Niri) {
match self {
@@ -112,21 +112,13 @@ impl Backend {
}
}
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
match self {
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
match self {
Backend::Tty(tty) => tty.enabled_outputs(),
Backend::Winit(winit) => winit.enabled_outputs(),
}
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn gbm_device(
&self,
+411 -57
View File
@@ -1,6 +1,9 @@
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::iter::zip;
use std::num::NonZeroU64;
use std::os::fd::AsFd;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::rc::Rc;
@@ -8,7 +11,8 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{io, mem};
use anyhow::{anyhow, Context};
use anyhow::{anyhow, bail, ensure, Context};
use bytemuck::cast_slice_mut;
use libc::dev_t;
use niri_config::Config;
use smithay::backend::allocator::dmabuf::Dmabuf;
@@ -34,6 +38,7 @@ use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken};
use smithay::reexports::drm::control::{
self, connector, crtc, property, Device, Mode as DrmMode, ModeFlags, ModeTypeFlags,
ResourceHandle,
};
use smithay::reexports::gbm::Modifier;
use smithay::reexports::input::Libinput;
@@ -50,12 +55,12 @@ use smithay_drm_extras::edid::EdidInfo;
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use super::RenderResult;
use super::{IpcOutputMap, RenderResult};
use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::shaders;
use crate::utils::get_monotonic_time;
use crate::render_helpers::{shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
@@ -79,8 +84,7 @@ pub struct Tty {
update_output_config_on_resume: bool,
// Whether the debug tinting is enabled.
debug_tint: bool,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
pub type TtyRenderer<'render> = MultiRenderer<
@@ -164,6 +168,9 @@ struct Surface {
name: String,
compositor: GbmDrmCompositor,
dmabuf_feedback: Option<SurfaceDmabufFeedback>,
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
pending_gamma_change: Option<Option<Vec<u16>>>,
/// Tracy frame that goes from vblank to vblank.
vblank_frame: Option<tracy_client::Frame>,
/// Frame name for the VBlank frame.
@@ -180,6 +187,13 @@ pub struct SurfaceDmabufFeedback {
pub scanout: DmabufFeedback,
}
struct GammaProps {
crtc: crtc::Handle,
gamma_lut: property::Handle,
gamma_lut_size: property::Handle,
previous_blob: Option<NonZeroU64>,
}
impl Tty {
pub fn new(
config: Rc<RefCell<Config>>,
@@ -251,8 +265,13 @@ impl Tty {
.context("error opening the primary GPU DRM node")?;
let primary_render_node = primary_node
.node_with_type(NodeType::Render)
.context("error getting the render node for the primary GPU")?
.context("error getting the render node for the primary GPU")?;
.and_then(Result::ok)
.unwrap_or_else(|| {
warn!(
"error getting the render node for the primary GPU; proceeding anyway"
);
primary_node
});
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
})?;
@@ -277,8 +296,7 @@ impl Tty {
dmabuf_global: None,
update_output_config_on_resume: false,
debug_tint: false,
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
ipc_outputs: Arc::new(Mutex::new(HashMap::new())),
})
}
@@ -382,6 +400,26 @@ impl Tty {
// Refresh the connectors.
self.device_changed(node.dev_id(), niri);
// Apply pending gamma changes and restore our existing gamma.
let device = self.devices.get_mut(&node).unwrap();
for (crtc, surface) in device.surfaces.iter_mut() {
if let Some(ramp) = surface.pending_gamma_change.take() {
let ramp = ramp.as_deref();
let res = if let Some(gamma_props) = &mut surface.gamma_props {
gamma_props.set_gamma(&device.drm, ramp)
} else {
set_gamma_for_crtc(&device.drm, *crtc, ramp)
};
if let Err(err) = res {
warn!("error applying pending gamma change: {err:?}");
}
} else if let Some(gamma_props) = &surface.gamma_props {
if let Err(err) = gamma_props.restore_gamma(&device.drm) {
warn!("error restoring gamma: {err:?}");
}
}
}
}
// Add new devices.
@@ -395,7 +433,7 @@ impl Tty {
self.on_output_config_changed(niri);
}
self.refresh_ipc_outputs();
self.refresh_ipc_outputs(niri);
niri.idle_notifier_state.notify_activity(&niri.seat);
niri.monitors_active = true;
@@ -443,7 +481,9 @@ impl Tty {
.single_renderer(&render_node)
.context("error creating renderer")?;
renderer.bind_wl_display(&niri.display_handle)?;
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding wl-display in EGL: {err:?}");
}
shaders::init(renderer.as_gles_renderer());
@@ -543,7 +583,7 @@ impl Tty {
}
}
self.refresh_ipc_outputs();
self.refresh_ipc_outputs(niri);
}
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
@@ -607,7 +647,7 @@ impl Tty {
self.gpu_manager.as_mut().remove_node(&device.render_node);
niri.event_loop.remove(device.token);
self.refresh_ipc_outputs();
self.refresh_ipc_outputs(niri);
}
fn connector_connected(
@@ -694,6 +734,26 @@ impl Tty {
}
debug!("picking mode: {mode:?}");
// We only use 8888 RGB formats, so set max bpc to 8 to allow more types of links to run.
match set_max_bpc(&device.drm, connector.handle(), 8) {
Ok(bpc) => debug!("set max bpc to {bpc}"),
Err(err) => debug!("error setting max bpc: {err:?}"),
}
let mut gamma_props = GammaProps::new(&device.drm, crtc)
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
.ok();
// Reset gamma in case it was set before.
let res = if let Some(gamma_props) = &mut gamma_props {
gamma_props.set_gamma(&device.drm, None)
} else {
set_gamma_for_crtc(&device.drm, crtc, None)
};
if let Err(err) = res {
debug!("error resetting gamma: {err:?}");
}
let surface = device
.drm
.create_surface(crtc, mode, &[connector.handle()])?;
@@ -804,6 +864,8 @@ impl Tty {
name: output_name.clone(),
compositor,
dmabuf_feedback,
gamma_props,
pending_gamma_change: None,
vblank_frame: None,
vblank_frame_name,
time_since_presentation_plot_name,
@@ -815,17 +877,10 @@ impl Tty {
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(&mut state.backend);
state.niri.queue_redraw(output);
state.niri.queue_redraw(&output);
});
Ok(())
@@ -859,10 +914,6 @@ impl Tty {
} else {
error!("missing output for crtc {crtc:?}");
};
self.enabled_outputs.lock().unwrap().remove(&surface.name);
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
}
fn on_vblank(
@@ -996,7 +1047,7 @@ impl Tty {
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued(_) => unreachable!(),
RedrawState::Queued => unreachable!(),
RedrawState::WaitingForVBlank { redraw_needed } => redraw_needed,
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
@@ -1008,7 +1059,7 @@ impl Tty {
.non_continuous_frame(surface.vblank_frame_name);
surface.vblank_frame = Some(vblank_frame);
niri.queue_redraw(output);
niri.queue_redraw(&output);
} else {
niri.send_frame_callbacks(&output);
}
@@ -1030,18 +1081,18 @@ impl Tty {
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued(_) => unreachable!(),
RedrawState::Queued => unreachable!(),
RedrawState::WaitingForVBlank { .. } => unreachable!(),
RedrawState::WaitingForEstimatedVBlank(_) => (),
// The timer fired just in front of a redraw.
RedrawState::WaitingForEstimatedVBlankAndQueued((_, idle)) => {
output_state.redraw_state = RedrawState::Queued(idle);
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => {
output_state.redraw_state = RedrawState::Queued;
return;
}
}
if output_state.unfinished_animations_remain {
niri.queue_redraw(output);
niri.queue_redraw(&output);
} else {
niri.send_frame_callbacks(&output);
}
@@ -1103,7 +1154,8 @@ impl Tty {
};
// Render the elements.
let elements = niri.render::<TtyRenderer>(&mut renderer, output, true);
let elements =
niri.render::<TtyRenderer>(&mut renderer, output, true, RenderTarget::Output);
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
@@ -1139,10 +1191,10 @@ impl Tty {
};
match mem::replace(&mut output_state.redraw_state, new_state) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued(_) => (),
RedrawState::Queued => (),
RedrawState::WaitingForVBlank { .. } => unreachable!(),
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued((token, _)) => {
RedrawState::WaitingForEstimatedVBlankAndQueued(token) => {
niri.event_loop.remove(token);
}
};
@@ -1236,12 +1288,57 @@ impl Tty {
}
}
fn refresh_ipc_outputs(&self) {
pub fn get_gamma_size(&self, output: &Output) -> anyhow::Result<u32> {
let tty_state = output.user_data().get::<TtyOutputState>().unwrap();
let crtc = tty_state.crtc;
let device = self
.devices
.get(&tty_state.node)
.context("missing device")?;
let surface = device.surfaces.get(&crtc).context("missing surface")?;
if let Some(gamma_props) = &surface.gamma_props {
gamma_props.gamma_size(&device.drm)
} else {
let info = device
.drm
.get_crtc(crtc)
.context("error getting crtc info")?;
Ok(info.gamma_length())
}
}
pub fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> anyhow::Result<()> {
let tty_state = output.user_data().get::<TtyOutputState>().unwrap();
let crtc = tty_state.crtc;
let device = self
.devices
.get_mut(&tty_state.node)
.context("missing device")?;
let surface = device.surfaces.get_mut(&crtc).context("missing surface")?;
// Cannot change properties while the device is inactive.
if !self.session.is_active() {
surface.pending_gamma_change = Some(ramp);
return Ok(());
}
let ramp = ramp.as_deref();
if let Some(gamma_props) = &mut surface.gamma_props {
gamma_props.set_gamma(&device.drm, ramp)
} else {
set_gamma_for_crtc(&device.drm, crtc, ramp)
}
}
fn refresh_ipc_outputs(&self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
let mut ipc_outputs = HashMap::new();
for device in self.devices.values() {
for (node, device) in &self.devices {
for (connector, crtc) in device.drm_scanner.crtcs() {
let connector_name = format!(
"{}-{}",
@@ -1278,6 +1375,7 @@ impl Tty {
width: m.size().0,
height: m.size().1,
refresh_rate: Mode::from(*m).refresh as u32,
is_preferred: m.mode_type().contains(ModeTypeFlags::PREFERRED),
}
})
.collect();
@@ -1292,30 +1390,38 @@ impl Tty {
}
}
let output = niri_ipc::Output {
let logical = niri
.global_space
.outputs()
.find(|output| {
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
tty_state.node == *node && tty_state.crtc == crtc
})
.map(logical_output);
let ipc_output = niri_ipc::Output {
name: connector_name.clone(),
make,
model,
physical_size,
modes,
current_mode,
logical,
};
ipc_outputs.insert(connector_name, output);
ipc_outputs.insert(connector_name, ipc_output);
}
}
self.ipc_outputs.replace(ipc_outputs);
let mut guard = self.ipc_outputs.lock().unwrap();
*guard = ipc_outputs;
niri.ipc_outputs_changed = true;
}
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
self.ipc_outputs.clone()
}
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.enabled_outputs.clone()
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn primary_gbm_device(&self) -> Option<GbmDevice<DrmDeviceFd>> {
self.devices.get(&self.primary_node).map(|d| d.gbm.clone())
@@ -1434,7 +1540,7 @@ impl Tty {
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);
niri.output_resized(&output);
}
for (connector, crtc) in device.drm_scanner.crtcs() {
@@ -1479,7 +1585,7 @@ impl Tty {
}
}
self.refresh_ipc_outputs();
self.refresh_ipc_outputs(niri);
}
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
@@ -1487,6 +1593,145 @@ impl Tty {
}
}
impl GammaProps {
fn new(device: &DrmDevice, crtc: crtc::Handle) -> anyhow::Result<Self> {
let mut gamma_lut = None;
let mut gamma_lut_size = None;
let props = device
.get_properties(crtc)
.context("error getting properties")?;
for (prop, _) in props {
let Ok(info) = device.get_property(prop) else {
continue;
};
let Ok(name) = info.name().to_str() else {
continue;
};
match name {
"GAMMA_LUT" => {
ensure!(
matches!(info.value_type(), property::ValueType::Blob),
"wrong GAMMA_LUT value type"
);
gamma_lut = Some(prop);
}
"GAMMA_LUT_SIZE" => {
ensure!(
matches!(info.value_type(), property::ValueType::UnsignedRange(_, _)),
"wrong GAMMA_LUT_SIZE value type"
);
gamma_lut_size = Some(prop);
}
_ => (),
}
}
let gamma_lut = gamma_lut.context("missing GAMMA_LUT property")?;
let gamma_lut_size = gamma_lut_size.context("missing GAMMA_LUT_SIZE property")?;
Ok(Self {
crtc,
gamma_lut,
gamma_lut_size,
previous_blob: None,
})
}
fn gamma_size(&self, device: &DrmDevice) -> anyhow::Result<u32> {
let value = get_drm_property(device, self.crtc, self.gamma_lut_size)
.context("missing GAMMA_LUT_SIZE property")?;
Ok(value as u32)
}
fn set_gamma(&mut self, device: &DrmDevice, gamma: Option<&[u16]>) -> anyhow::Result<()> {
let _span = tracy_client::span!("GammaProps::set_gamma");
let blob = if let Some(gamma) = gamma {
let gamma_size = self
.gamma_size(device)
.context("error getting gamma size")? as usize;
ensure!(gamma.len() == gamma_size * 3, "wrong gamma length");
#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct drm_color_lut {
pub red: u16,
pub green: u16,
pub blue: u16,
pub reserved: u16,
}
let (red, rest) = gamma.split_at(gamma_size);
let (blue, green) = rest.split_at(gamma_size);
let mut data = zip(zip(red, blue), green)
.map(|((&red, &green), &blue)| drm_color_lut {
red,
green,
blue,
reserved: 0,
})
.collect::<Vec<_>>();
let data = cast_slice_mut(&mut data);
let blob = drm_ffi::mode::create_property_blob(device.as_fd(), data)
.context("error creating property blob")?;
NonZeroU64::new(u64::from(blob.blob_id))
} else {
None
};
{
let _span = tracy_client::span!("set_property");
let blob = blob.map(NonZeroU64::get).unwrap_or(0);
device
.set_property(
self.crtc,
self.gamma_lut,
property::Value::Blob(blob).into(),
)
.context("error setting GAMMA_LUT")
.map_err(|err| {
if blob != 0 {
// Destroy the blob we just allocated.
if let Err(err) = device.destroy_property_blob(blob) {
warn!("error destroying GAMMA_LUT property blob: {err:?}");
}
}
err
})?;
}
if let Some(blob) = mem::replace(&mut self.previous_blob, blob) {
if let Err(err) = device.destroy_property_blob(blob.get()) {
warn!("error destroying previous GAMMA_LUT blob: {err:?}");
}
}
Ok(())
}
fn restore_gamma(&self, device: &DrmDevice) -> anyhow::Result<()> {
let _span = tracy_client::span!("GammaProps::restore_gamma");
let blob = self.previous_blob.map(NonZeroU64::get).unwrap_or(0);
device
.set_property(
self.crtc,
self.gamma_lut,
property::Value::Blob(blob).into(),
)
.context("error setting GAMMA_LUT")?;
Ok(())
}
}
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
let path = config.debug.render_drm_device.as_ref()?;
debug!("attempting to use render node from config: {path:?}");
@@ -1507,6 +1752,14 @@ fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
}
} else {
warn!("DRM node {path:?} is not a render node");
// Gracefully handle misconfiguration on regular desktop systems.
if let Some(Ok(render_node)) = node.node_with_type(NodeType::Render) {
return Some((node, render_node));
}
warn!("could not get render node for DRM node {path:?}; proceeding anyway");
return Some((node, node));
}
}
Err(err) => {
@@ -1568,26 +1821,47 @@ fn surface_dmabuf_feedback(
Ok(SurfaceDmabufFeedback { render, scanout })
}
fn find_drm_property(drm: &DrmDevice, crtc: crtc::Handle, name: &str) -> Option<property::Handle> {
let props = match drm.get_properties(crtc) {
fn find_drm_property(
drm: &DrmDevice,
resource: impl ResourceHandle,
name: &str,
) -> Option<(property::Handle, property::RawValue)> {
let props = match drm.get_properties(resource) {
Ok(props) => props,
Err(err) => {
warn!("error getting CRTC properties: {err:?}");
warn!("error getting properties: {err:?}");
return None;
}
};
let (handles, _) = props.as_props_and_values();
handles.iter().find_map(|handle| {
let info = drm.get_property(*handle).ok()?;
props.into_iter().find_map(|(handle, value)| {
let info = drm.get_property(handle).ok()?;
let n = info.name().to_str().ok()?;
(n == name).then_some(*handle)
(n == name).then_some((handle, value))
})
}
fn get_drm_property(
drm: &DrmDevice,
resource: impl ResourceHandle,
prop: property::Handle,
) -> Option<property::RawValue> {
let props = match drm.get_properties(resource) {
Ok(props) => props,
Err(err) => {
warn!("error getting properties: {err:?}");
return None;
}
};
props
.into_iter()
.find_map(|(handle, value)| (handle == prop).then_some(value))
}
fn set_crtc_active(drm: &DrmDevice, crtc: crtc::Handle, active: bool) {
let Some(prop) = find_drm_property(drm, crtc, "ACTIVE") else {
let Some((prop, _)) = find_drm_property(drm, crtc, "ACTIVE") else {
return;
};
@@ -1645,10 +1919,10 @@ fn queue_estimated_vblank_timer(
let output_state = niri.output_state.get_mut(&output).unwrap();
match mem::take(&mut output_state.redraw_state) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued(_) => (),
RedrawState::Queued => (),
RedrawState::WaitingForVBlank { .. } => unreachable!(),
RedrawState::WaitingForEstimatedVBlank(token)
| RedrawState::WaitingForEstimatedVBlankAndQueued((token, _)) => {
| RedrawState::WaitingForEstimatedVBlankAndQueued(token) => {
output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token);
return;
}
@@ -1768,6 +2042,86 @@ fn get_edid_info(device: &DrmDevice, connector: connector::Handle) -> Option<Edi
}
}
fn set_max_bpc(device: &DrmDevice, connector: connector::Handle, bpc: u64) -> anyhow::Result<u64> {
let props = device
.get_properties(connector)
.context("error getting properties")?;
for (prop, value) in props {
let info = device
.get_property(prop)
.context("error getting property")?;
if info.name().to_str() != Ok("max bpc") {
continue;
}
let property::ValueType::UnsignedRange(min, max) = info.value_type() else {
bail!("wrong property type")
};
let bpc = bpc.clamp(min, max);
let property::Value::UnsignedRange(value) = info.value_type().convert_value(value) else {
bail!("wrong property type")
};
if value == bpc {
return Ok(bpc);
}
device
.set_property(connector, prop, property::Value::UnsignedRange(bpc).into())
.context("error setting property")?;
return Ok(bpc);
}
Err(anyhow!("couldn't find max bpc property"))
}
pub fn set_gamma_for_crtc(
device: &DrmDevice,
crtc: crtc::Handle,
ramp: Option<&[u16]>,
) -> anyhow::Result<()> {
let _span = tracy_client::span!("set_gamma_for_crtc");
let info = device.get_crtc(crtc).context("error getting crtc info")?;
let gamma_length = info.gamma_length() as usize;
ensure!(gamma_length != 0, "setting gamma is not supported");
let mut temp;
let ramp = if let Some(ramp) = ramp {
ensure!(ramp.len() == gamma_length * 3, "wrong gamma length");
ramp
} else {
let _span = tracy_client::span!("generate linear gamma");
// The legacy API provides no way to reset the gamma, so set a linear one manually.
temp = vec![0u16; gamma_length * 3];
let (red, rest) = temp.split_at_mut(gamma_length);
let (green, blue) = rest.split_at_mut(gamma_length);
let denom = gamma_length as u64 - 1;
for (i, ((r, g), b)) in zip(zip(red, green), blue).enumerate() {
let value = (0xFFFFu64 * i as u64 / denom) as u16;
*r = value;
*g = value;
*b = value;
}
&temp
};
let (red, ramp) = ramp.split_at(gamma_length);
let (green, blue) = ramp.split_at(gamma_length);
device
.set_gamma(crtc, red, green, blue)
.context("error setting gamma")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
+29 -27
View File
@@ -17,18 +17,17 @@ use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_pre
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::WindowBuilder;
use super::RenderResult;
use super::{IpcOutputMap, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::shaders;
use crate::utils::get_monotonic_time;
use crate::render_helpers::{shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
config: Rc<RefCell<Config>>,
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
impl Winit {
@@ -60,7 +59,7 @@ impl Winit {
output.set_preferred(mode);
let physical_properties = output.physical_properties();
let ipc_outputs = Rc::new(RefCell::new(HashMap::from([(
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
niri_ipc::Output {
name: output.name(),
@@ -71,16 +70,13 @@ impl Winit {
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,
is_preferred: true,
}],
current_mode: Some(0),
logical: Some(logical_output(&output)),
},
)])));
let enabled_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
output.clone(),
)])));
let damage_tracker = OutputDamageTracker::from_output(&output);
event_loop
@@ -97,18 +93,24 @@ impl Winit {
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;
{
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
let output = ipc_outputs.get_mut("winit").unwrap();
let mode = &mut output.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;
if let Some(logical) = output.logical.as_mut() {
logical.width = size.w as u32;
logical.height = size.h as u32;
}
state.niri.ipc_outputs_changed = true;
}
state.niri.output_resized(winit.output.clone());
state.niri.output_resized(&winit.output);
}
WinitEvent::Input(event) => state.process_input_event(event),
WinitEvent::Focus(_) => (),
WinitEvent::Redraw => state
.niri
.queue_redraw(state.backend.winit().output.clone()),
WinitEvent::Redraw => state.niri.queue_redraw(&state.backend.winit().output),
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
})
.unwrap();
@@ -119,7 +121,6 @@ impl Winit {
backend,
damage_tracker,
ipc_outputs,
enabled_outputs,
})
}
@@ -149,7 +150,12 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let elements = niri.render::<GlesRenderer>(self.backend.renderer(), output, true);
let elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
// Hand them over to winit.
self.backend.bind().unwrap();
@@ -193,7 +199,7 @@ impl Winit {
let output_state = niri.output_state.get_mut(output).unwrap();
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
RedrawState::Idle => unreachable!(),
RedrawState::Queued(_) => (),
RedrawState::Queued => (),
RedrawState::WaitingForVBlank { .. } => unreachable!(),
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
@@ -225,11 +231,7 @@ impl Winit {
}
}
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
self.ipc_outputs.clone()
}
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.enabled_outputs.clone()
}
}
+2
View File
@@ -54,6 +54,8 @@ pub enum Sub {
pub enum Msg {
/// List connected outputs.
Outputs,
/// Print information about the focused window.
FocusedWindow,
/// Perform an action.
Action {
#[command(subcommand)]
+5 -3
View File
@@ -47,7 +47,7 @@ impl DBusServers {
}
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
let display_config = DisplayConfig::new(backend.enabled_outputs());
let display_config = DisplayConfig::new(backend.ipc_outputs());
dbus.conn_display_config = try_start(display_config);
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
@@ -67,7 +67,7 @@ impl DBusServers {
dbus.conn_screen_shot = try_start(screenshot);
#[cfg(feature = "xdp-gnome-screencast")]
{
if niri.pipewire.is_some() {
let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop
.insert_source(from_screen_cast, {
@@ -80,8 +80,10 @@ impl DBusServers {
}
})
.unwrap();
let screen_cast = ScreenCast::new(backend.enabled_outputs(), to_niri);
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
dbus.conn_screen_cast = try_start(screen_cast);
} else {
warn!("disabling screencast support because we couldn't start PipeWire");
}
}
+74 -28
View File
@@ -2,15 +2,15 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde::Serialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
pub struct DisplayConfig {
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
#[derive(Serialize, Type)]
@@ -53,12 +53,14 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let mut monitors: Vec<Monitor> = self
.enabled_outputs
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
.ipc_outputs
.lock()
.unwrap()
.keys()
.map(|c| {
.iter()
// Take only enabled outputs.
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
.map(|(c, output)| {
// Loosely matches the check in Mutter.
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
@@ -78,38 +80,82 @@ impl DisplayConfig {
OwnedValue::from(is_laptop_panel),
);
Monitor {
let mut modes: Vec<Mode> = output
.modes
.iter()
.map(|m| {
let niri_ipc::Mode {
width,
height,
refresh_rate,
is_preferred,
} = *m;
let refresh = refresh_rate as f64 / 1000.;
Mode {
id: format!("{width}x{height}@{refresh:.3}"),
width: i32::from(width),
height: i32::from(height),
refresh_rate: refresh,
preferred_scale: 1.,
supported_scales: vec![1., 2., 3.],
properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
}
})
.collect();
modes[output.current_mode.unwrap()]
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
let monitor = Monitor {
names: (c.clone(), String::new(), String::new(), serial),
modes: vec![],
modes,
properties,
}
};
let logical = output.logical.as_ref().unwrap();
let transform = match logical.transform {
niri_ipc::Transform::Normal => 0,
niri_ipc::Transform::_90 => 1,
niri_ipc::Transform::_180 => 2,
niri_ipc::Transform::_270 => 3,
niri_ipc::Transform::Flipped => 4,
niri_ipc::Transform::Flipped90 => 5,
niri_ipc::Transform::Flipped180 => 6,
niri_ipc::Transform::Flipped270 => 7,
};
let logical_monitor = LogicalMonitor {
x: logical.x,
y: logical.y,
scale: logical.scale,
transform,
is_primary: false,
monitors: vec![monitor.names.clone()],
properties: HashMap::new(),
};
(monitor, logical_monitor)
})
.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");
let a_is_builtin = a.0.properties.contains_key("display-name");
let b_is_builtin = b.0.properties.contains_key("display-name");
a_is_builtin
.cmp(&b_is_builtin)
.reverse()
.then_with(|| a.names.0.cmp(&b.names.0))
.then_with(|| a.0.names.0.cmp(&b.0.names.0))
});
let logical_monitors = monitors
.iter()
.map(|m| LogicalMonitor {
x: 0,
y: 0,
scale: 1.,
transform: 0,
is_primary: false,
monitors: vec![m.names.clone()],
properties: HashMap::new(),
})
.collect();
Ok((0, monitors, logical_monitors, HashMap::new()))
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
Ok((0, monitors, logical_monitors, properties))
}
#[dbus_interface(signal)]
@@ -117,8 +163,8 @@ impl DisplayConfig {
}
impl DisplayConfig {
pub fn new(enabled_outputs: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { enabled_outputs }
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
Self { ipc_outputs }
}
}
+33 -20
View File
@@ -10,11 +10,11 @@ use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Valu
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start;
use crate::utils::output_size;
use crate::backend::IpcOutputMap;
#[derive(Clone)]
pub struct ScreenCast {
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
@@ -23,10 +23,11 @@ pub struct ScreenCast {
#[derive(Clone)]
pub struct Session {
id: usize,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
#[allow(clippy::type_complexity)]
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
stopped: Arc<AtomicBool>,
}
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
@@ -48,7 +49,8 @@ struct RecordMonitorProperties {
#[derive(Clone)]
pub struct Stream {
output: Output,
// FIXME: update on scale changes and whatnot.
output: niri_ipc::Output,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
@@ -57,6 +59,8 @@ pub struct Stream {
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
struct StreamParameters {
/// Position of the stream in logical coordinates.
position: (i32, i32),
/// Size of the stream in logical coordinates.
size: (i32, i32),
}
@@ -64,7 +68,7 @@ struct StreamParameters {
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
output: Output,
output: String,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
},
@@ -92,11 +96,7 @@ 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.enabled_outputs.clone(),
self.to_niri.clone(),
);
let session = Session::new(session_id, self.ipc_outputs.clone(), self.to_niri.clone());
match server.at(&path, session.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -136,6 +136,11 @@ impl Session {
) {
debug!("stop");
if self.stopped.swap(true, Ordering::SeqCst) {
// Already stopped.
return;
}
Session::closed(&ctxt).await.unwrap();
if let Err(err) = self.to_niri.send(ScreenCastToNiri::StopCast {
@@ -163,10 +168,14 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.enabled_outputs.lock().unwrap().get(connector).cloned() else {
let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else {
return Err(fdo::Error::Failed("no such monitor".to_owned()));
};
if output.logical.is_none() {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
static NUMBER: AtomicUsize = AtomicUsize::new(0);
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
@@ -176,7 +185,7 @@ impl Session {
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let stream = Stream::new(output, cursor_mode, self.to_niri.clone());
let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
@@ -205,18 +214,21 @@ impl Stream {
#[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters {
let size = output_size(&self.output).into();
StreamParameters { size }
let logical = self.output.logical.as_ref().unwrap();
StreamParameters {
position: (logical.x, logical.y),
size: (logical.width as i32, logical.height as i32),
}
}
}
impl ScreenCast {
pub fn new(
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
enabled_outputs,
ipc_outputs,
to_niri,
sessions: Arc::new(Mutex::new(vec![])),
}
@@ -241,14 +253,15 @@ impl Start for ScreenCast {
impl Session {
pub fn new(
id: usize,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
enabled_outputs,
ipc_outputs,
streams: Arc::new(Mutex::new(vec![])),
to_niri,
stopped: Arc::new(AtomicBool::new(false)),
}
}
}
@@ -263,7 +276,7 @@ impl Drop for Session {
impl Stream {
pub fn new(
output: Output,
output: niri_ipc::Output,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
@@ -282,7 +295,7 @@ impl Stream {
let msg = ScreenCastToNiri::StartCast {
session_id,
output: self.output.clone(),
output: self.output.name.clone(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
};
+30 -20
View File
@@ -17,8 +17,7 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use crate::niri::{ClientState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, Unmapped};
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@@ -109,22 +108,22 @@ impl CompositorHandler for State {
window.on_commit();
let (width, is_full_width, output) =
let (rules, width, is_full_width, output) =
if let InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
..
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
(width, is_full_width, output)
(rules, width, is_full_width, output)
} else {
error!("window map must happen after initial configure");
(None, false, None)
(ResolvedWindowRules::empty(), None, false, None)
};
let parent = window
@@ -141,27 +140,35 @@ impl CompositorHandler for State {
.filter(|(_, parent_output)| {
output.is_none() || output.as_ref() == Some(*parent_output)
})
.map(|(window, _)| window.clone());
.map(|(mapped, _)| mapped.window.clone());
let win = window.clone();
let mapped = Mapped::new(window, rules);
let window = mapped.window.clone();
let output = if let Some(p) = parent {
// Open dialogs immediately to the right of their parent window.
self.niri
.layout
.add_window_right_of(&p, win, width, is_full_width)
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let Some(output) = &output {
self.niri
.layout
.add_window_on_output(output, win, width, is_full_width);
.add_window_on_output(output, mapped, width, is_full_width);
Some(output)
} else {
self.niri.layout.add_window(win, width, is_full_width)
self.niri.layout.add_window(mapped, width, is_full_width)
};
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
self.niri.queue_redraw(output);
let new_active_window =
self.niri.layout.active_window().map(|(m, _)| &m.window);
if new_active_window == Some(&window) {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(&output);
}
return;
}
@@ -176,8 +183,9 @@ impl CompositorHandler for State {
}
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some(win_out) = self.niri.layout.find_window_and_output(surface) {
let (window, output) = clone2(win_out);
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
@@ -198,7 +206,7 @@ impl CompositorHandler for State {
let unmapped = Unmapped::new(window);
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
self.niri.queue_redraw(output);
self.niri.queue_redraw(&output);
return;
}
@@ -208,7 +216,7 @@ impl CompositorHandler for State {
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window, &output);
self.niri.queue_redraw(output);
self.niri.queue_redraw(&output);
return;
}
@@ -217,10 +225,12 @@ impl CompositorHandler for State {
// This is a commit of a non-root or a non-toplevel root.
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
if let Some((window, output)) = root_window_output.map(clone2) {
if let Some((mapped, output)) = root_window_output {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
self.niri.layout.update_window(&window);
self.niri.queue_redraw(output);
self.niri.queue_redraw(&output);
return;
}
@@ -228,7 +238,7 @@ impl CompositorHandler for State {
self.popups_handle_commit(surface);
if let Some(popup) = self.niri.popups.find_popup(surface) {
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(output.clone());
self.niri.queue_redraw(&output.clone());
}
}
@@ -253,7 +263,7 @@ impl CompositorHandler for State {
for (output, state) in &self.niri.output_state {
if let Some(lock_surface) = &state.lock_surface {
if lock_surface.wl_surface() == surface {
self.niri.queue_redraw(output.clone());
self.niri.queue_redraw(&output.clone());
break;
}
}
+2 -2
View File
@@ -50,7 +50,7 @@ impl WlrLayerShellHandler for State {
None
};
if let Some(output) = output {
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
@@ -107,6 +107,6 @@ impl State {
}
drop(map);
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
+44 -13
View File
@@ -58,9 +58,10 @@ use crate::niri::{ClientState, State};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
use crate::utils::output_size;
use crate::{delegate_foreign_toplevel, delegate_screencopy};
use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy};
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
@@ -143,7 +144,7 @@ impl InputMethodHandler for State {
self.niri
.layout
.find_window_and_output(parent)
.map(|(window, _)| window.geometry())
.map(|(mapped, _)| mapped.window.geometry())
.unwrap_or_default()
}
}
@@ -332,25 +333,24 @@ impl ForeignToplevelHandler for 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();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = mapped.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().expect("no x11 support").send_close();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
mapped.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 let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !window
if !mapped
.toplevel()
.expect("no x11 support")
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
@@ -358,13 +358,13 @@ impl ForeignToplevelHandler for State {
return;
}
let window = window.clone();
let window = mapped.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);
.move_window_to_output(&window, &requested_output);
}
}
@@ -373,8 +373,8 @@ impl ForeignToplevelHandler for State {
}
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();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
}
}
@@ -440,3 +440,34 @@ impl DrmLeaseHandler for State {
delegate_drm_lease!(State);
delegate_viewporter!(State);
impl GammaControlHandler for State {
fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState {
&mut self.niri.gamma_control_manager_state
}
fn get_gamma_size(&mut self, output: &Output) -> Option<u32> {
match self.backend.tty().get_gamma_size(output) {
Ok(0) => None, // Setting gamma is not supported.
Ok(size) => Some(size),
Err(err) => {
warn!(
"error getting gamma size for output {}: {err:?}",
output.name()
);
None
}
}
}
fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> Option<()> {
match self.backend.tty().set_gamma(output, ramp) {
Ok(()) => Some(()),
Err(err) => {
warn!("error setting gamma for output {}: {err:?}", output.name());
None
}
}
}
}
delegate_gamma_control!(State);
+47 -98
View File
@@ -1,4 +1,3 @@
use niri_config::{Match, WindowRule};
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@@ -20,7 +19,7 @@ use smithay::wayland::shell::wlr_layer::Layer;
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
XdgShellState, XdgToplevelSurfaceData,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
@@ -29,84 +28,7 @@ use smithay::{
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped};
fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
if let Some(app_id_re) = &m.app_id {
let Some(app_id) = &role.app_id else {
return false;
};
if !app_id_re.is_match(app_id) {
return false;
}
}
if let Some(title_re) = &m.title {
let Some(title) = &role.title else {
return false;
};
if !title_re.is_match(title) {
return false;
}
}
true
}
pub fn resolve_window_rules(
rules: &[WindowRule],
toplevel: &ToplevelSurface,
) -> ResolvedWindowRules {
let _span = tracy_client::span!("resolve_window_rules");
let mut resolved = ResolvedWindowRules::default();
with_states(toplevel.wl_surface(), |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
let mut open_on_output = None;
for rule in rules {
if !(rule.matches.is_empty() || rule.matches.iter().any(|m| window_matches(&role, m))) {
continue;
}
if rule.excludes.iter().any(|m| window_matches(&role, m)) {
continue;
}
if let Some(x) = rule
.default_column_width
.as_ref()
.map(|d| d.0.map(ColumnWidth::from))
{
resolved.default_width = Some(x);
}
if let Some(x) = rule.open_on_output.as_deref() {
open_on_output = Some(x);
}
if let Some(x) = rule.open_maximized {
resolved.open_maximized = Some(x);
}
if let Some(x) = rule.open_fullscreen {
resolved.open_fullscreen = Some(x);
}
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
});
resolved
}
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
@@ -213,9 +135,7 @@ impl XdgShellHandler for State {
}
let layout_focus = self.niri.layout.focus();
if Some(&root)
!= layout_focus.map(|win| win.toplevel().expect("no x11 support").wl_surface())
{
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
@@ -278,18 +198,18 @@ impl XdgShellHandler for State {
) {
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
if let Some((window, current_output)) = self
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
let window = window.clone();
let window = mapped.window.clone();
if let Some(requested_output) = requested_output {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
.move_window_to_output(&window, &requested_output);
}
}
@@ -358,12 +278,12 @@ impl XdgShellHandler for State {
}
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
if let Some((window, _)) = self
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
let window = window.clone();
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
// A configure is required in response to this event regardless if there are pending
@@ -453,20 +373,30 @@ impl XdgShellHandler for State {
.layout
.find_window_and_output(surface.wl_surface());
let Some((window, output)) = win_out.map(clone2) else {
let Some((mapped, output)) = win_out else {
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
// involving laptop going to sleep and resuming.
error!("toplevel missing from both unmapped_windows and layout");
return;
};
let window = mapped.window.clone();
let output = output.clone();
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window);
self.niri.queue_redraw(output);
if was_active {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(&output);
}
fn popup_destroyed(&mut self, surface: PopupSurface) {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
self.niri.queue_redraw(output.clone());
self.niri.queue_redraw(&output.clone());
}
}
@@ -559,6 +489,10 @@ impl State {
return;
};
let config = self.niri.config.borrow();
let rules =
ResolvedWindowRules::compute(&config.window_rules, WindowRef::Unmapped(unmapped));
let Unmapped { window, state } = unmapped;
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
@@ -566,9 +500,6 @@ impl State {
return;
};
let config = self.niri.config.borrow();
let rules = resolve_window_rules(&config.window_rules, toplevel);
// Pick the target monitor. First, check if we had an output set in the window rules.
let mon = rules
.open_on_output
@@ -725,8 +656,8 @@ impl State {
};
// Figure out if the root is a window or a layer surface.
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, window, output);
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window, output);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
@@ -800,11 +731,29 @@ impl State {
}
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
let resolve = || resolve_window_rules(&self.niri.config.borrow().window_rules, toplevel);
let config = self.niri.config.borrow();
let window_rules = &config.window_rules;
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
let new_rules =
ResolvedWindowRules::compute(window_rules, WindowRef::Unmapped(unmapped));
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = resolve();
*rules = new_rules;
}
} else if let Some((mapped, output)) = self
.niri
.layout
.find_window_and_output_mut(toplevel.wl_surface())
{
if mapped.recompute_window_rules(window_rules) {
drop(config);
let output = output.cloned();
let window = mapped.window.clone();
self.niri.layout.update_window(&window);
if let Some(output) = output {
self.niri.queue_redraw(&output);
}
}
}
}
+511 -162
View File
File diff suppressed because it is too large Load Diff
+77 -4
View File
@@ -4,7 +4,7 @@ use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use anyhow::{anyhow, bail, Context};
use niri_ipc::{Mode, Output, Reply, Request, Response};
use niri_ipc::{LogicalOutput, Mode, Output, Reply, Request, Response};
use crate::cli::Msg;
@@ -21,6 +21,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let request = match &msg {
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::Action { action } => Request::Action(action.clone()),
};
let mut buf = serde_json::to_vec(&request).unwrap();
@@ -66,6 +67,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
physical_size,
modes,
current_mode,
logical,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
@@ -78,9 +80,11 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz");
let preferred = if is_preferred { " (preferred)" } else { "" };
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
} else {
println!(" Disabled");
}
@@ -91,19 +95,88 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!(" Physical size: unknown");
}
if let Some(logical) = logical {
let LogicalOutput {
x,
y,
width,
height,
scale,
transform,
} = logical;
println!(" Logical position: {x}, {y}");
println!(" Logical size: {width}x{height}");
println!(" Scale: {scale}");
let transform = match transform {
niri_ipc::Transform::Normal => "normal",
niri_ipc::Transform::_90 => "90° counter-clockwise",
niri_ipc::Transform::_180 => "180°",
niri_ipc::Transform::_270 => "270° counter-clockwise",
niri_ipc::Transform::Flipped => "flipped horizontally",
niri_ipc::Transform::Flipped90 => {
"90° counter-clockwise, flipped horizontally"
}
niri_ipc::Transform::Flipped180 => "flipped vertically",
niri_ipc::Transform::Flipped270 => {
"270° counter-clockwise, flipped horizontally"
}
};
println!(" Transform: {transform}");
}
println!(" Available modes:");
for mode in modes {
for (idx, mode) in modes.into_iter().enumerate() {
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
println!(" {width}x{height}@{refresh:.3}");
let is_current = Some(idx) == current_mode;
let qualifier = match (is_current, is_preferred) {
(true, true) => " (current, preferred)",
(true, false) => " (current)",
(false, true) => " (preferred)",
(false, false) => "",
};
println!(" {width}x{height}@{refresh:.3}{qualifier}");
}
println!();
}
}
Msg::FocusedWindow => {
let Response::FocusedWindow(window) = response else {
bail!("unexpected response: expected FocusedWindow, got {response:?}");
};
if json {
let window = serde_json::to_string(&window).context("error formatting response")?;
println!("{window}");
return Ok(());
}
if let Some(window) = window {
println!("Focused window:");
if let Some(title) = window.title {
println!(" Title: \"{title}\"");
} else {
println!(" Title: (unset)");
}
if let Some(app_id) = window.app_id {
println!(" App ID: \"{app_id}\"");
} else {
println!(" App ID: (unset)");
}
} else {
println!("No window is focused.");
}
}
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
+29 -5
View File
@@ -1,8 +1,6 @@
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::sync::{Arc, Mutex};
use std::{env, io, process};
use anyhow::Context;
@@ -11,10 +9,14 @@ use directories::BaseDirs;
use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
use niri_ipc::{Request, Response};
use smithay::desktop::Window;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::rustix::fs::unlink;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::backend::IpcOutputMap;
use crate::niri::State;
pub struct IpcServer {
@@ -23,7 +25,8 @@ pub struct IpcServer {
struct ClientCtx {
event_loop: LoopHandle<'static, State>,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
ipc_focused_window: Arc<Mutex<Option<Window>>>,
}
impl IpcServer {
@@ -88,6 +91,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
let ctx = ClientCtx {
event_loop: state.niri.event_loop.clone(),
ipc_outputs: state.backend.ipc_outputs(),
ipc_focused_window: state.niri.ipc_focused_window.clone(),
};
let future = async move {
@@ -126,9 +130,29 @@ fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
let response = match request {
Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
Response::Outputs(ipc_outputs)
}
Request::FocusedWindow => {
let window = ctx.ipc_focused_window.lock().unwrap().clone();
let window = window.map(|window| {
let wl_surface = window.toplevel().expect("no X11 support").wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
niri_ipc::Window {
title: role.title.clone(),
app_id: role.app_id.clone(),
}
})
});
Response::FocusedWindow(window)
}
Request::Action(action) => {
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
+159 -172
View File
@@ -38,24 +38,19 @@ use niri_config::{CenterFocusedColumn, Config, Struts};
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::{AsRenderElements, Id};
use smithay::desktop::space::SpaceElement;
use smithay::desktop::Window;
use smithay::backend::renderer::element::Id;
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_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::shell::xdg::SurfaceCachedState;
use smithay::utils::{Logical, Point, Scale, Size, Transform};
use self::monitor::Monitor;
pub use self::monitor::MonitorRenderElement;
use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Workspace};
use crate::niri::WindowOffscreenId;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::utils::output_size;
use crate::window::ResolvedWindowRules;
pub mod focus_ring;
pub mod monitor;
@@ -69,7 +64,13 @@ niri_render_elements! {
}
}
pub trait LayoutElement: PartialEq {
pub trait LayoutElement {
/// Type that can be used as a unique ID of this element.
type Id: PartialEq;
/// Unique ID of this element.
fn id(&self) -> &Self::Id;
/// Visual size of the element.
///
/// This is what the user would consider the size, i.e. excluding CSD shadows and whatnot.
@@ -95,6 +96,8 @@ pub trait LayoutElement: PartialEq {
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>>;
fn request_size(&self, size: Size<i32, Logical>);
@@ -107,6 +110,10 @@ pub trait LayoutElement: PartialEq {
fn output_enter(&self, output: &Output);
fn output_leave(&self, output: &Output);
fn set_offscreen_element_id(&self, id: Option<Id>);
fn set_activated(&mut self, active: bool);
fn set_bounds(&self, bounds: Size<i32, Logical>);
fn send_pending_configure(&self);
/// Whether the element is currently fullscreen.
///
@@ -117,6 +124,11 @@ pub trait LayoutElement: PartialEq {
///
/// This *will* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_pending_fullscreen(&self) -> bool;
fn rules(&self) -> &ResolvedWindowRules;
/// Runs periodic clean-up tasks.
fn refresh(&self);
}
#[derive(Debug)]
@@ -216,120 +228,6 @@ impl Options {
}
}
impl LayoutElement for Window {
fn size(&self) -> Size<i32, Logical> {
self.geometry().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
Point::from((0, 0)) - self.geometry().loc
}
fn is_in_input_region(&self, point: Point<f64, Logical>) -> bool {
let surace_local = point + self.geometry().loc.to_f64();
SpaceElement::is_in_input_region(self, &surace_local)
}
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<LayoutElementRenderElement<R>> {
let buf_pos = location - self.geometry().loc;
self.render_elements(
renderer,
buf_pos.to_physical_precise_round(scale),
scale,
1.,
)
}
fn request_size(&self, size: Size<i32, Logical>) {
self.toplevel()
.expect("no x11 support")
.with_pending_state(|state| {
state.size = Some(size);
state.states.unset(xdg_toplevel::State::Fullscreen);
});
}
fn request_fullscreen(&self, size: Size<i32, Logical>) {
self.toplevel()
.expect("no x11 support")
.with_pending_state(|state| {
state.size = Some(size);
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
fn min_size(&self) -> Size<i32, Logical> {
with_states(
self.toplevel().expect("no x11 support").wl_surface(),
|state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.min_size
},
)
}
fn max_size(&self) -> Size<i32, Logical> {
with_states(
self.toplevel().expect("no x11 support").wl_surface(),
|state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.max_size
},
)
}
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool {
self.toplevel().expect("no x11 support").wl_surface() == wl_surface
}
fn set_preferred_scale_transform(&self, scale: i32, transform: Transform) {
self.with_surfaces(|surface, data| {
send_surface_state(surface, data, scale, transform);
});
}
fn has_ssd(&self) -> bool {
self.toplevel()
.expect("no x11 support")
.current_state()
.decoration_mode
== Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
}
fn output_enter(&self, output: &Output) {
let overlap = Rectangle::from_loc_and_size((0, 0), (i32::MAX, i32::MAX));
SpaceElement::output_enter(self, output, overlap)
}
fn output_leave(&self, output: &Output) {
SpaceElement::output_leave(self, output)
}
fn set_offscreen_element_id(&self, id: Option<Id>) {
let data = self.user_data().get_or_insert(WindowOffscreenId::default);
data.0.replace(id);
}
fn is_fullscreen(&self) -> bool {
self.toplevel()
.expect("no x11 support")
.current_state()
.states
.contains(xdg_toplevel::State::Fullscreen)
}
fn is_pending_fullscreen(&self) -> bool {
self.toplevel()
.expect("no x11 support")
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
}
}
impl<W: LayoutElement> Layout<W> {
pub fn new(config: &Config) -> Self {
Self::with_options(Options::from_config(config))
@@ -599,7 +497,7 @@ impl<W: LayoutElement> Layout<W> {
/// Returns an output that the window was added to, if there were any outputs.
pub fn add_window_right_of(
&mut self,
right_of: &W,
right_of: &W::Id,
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
@@ -681,13 +579,15 @@ impl<W: LayoutElement> Layout<W> {
);
}
pub fn remove_window(&mut self, window: &W) {
pub fn remove_window(&mut self, window: &W::Id) -> Option<W> {
let mut rv = None;
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for (idx, ws) in mon.workspaces.iter_mut().enumerate() {
if ws.has_window(window) {
ws.remove_window(window);
rv = Some(ws.remove_window(window));
// Clean up empty workspaces that are not active and not last.
if !ws.has_windows()
@@ -710,7 +610,7 @@ impl<W: LayoutElement> Layout<W> {
MonitorSet::NoOutputs { workspaces, .. } => {
for (idx, ws) in workspaces.iter_mut().enumerate() {
if ws.has_window(window) {
ws.remove_window(window);
rv = Some(ws.remove_window(window));
// Clean up empty workspaces.
if !ws.has_windows() {
@@ -722,9 +622,11 @@ impl<W: LayoutElement> Layout<W> {
}
}
}
rv
}
pub fn update_window(&mut self, window: &W) {
pub fn update_window(&mut self, window: &W::Id) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@@ -761,7 +663,33 @@ impl<W: LayoutElement> Layout<W> {
None
}
pub fn window_y(&self, window: &W) -> Option<i32> {
pub fn find_window_and_output_mut(
&mut self,
wl_surface: &WlSurface,
) -> Option<(&mut W, Option<&Output>)> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
return Some((window, Some(&mon.output)));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
return Some((window, None));
}
}
}
}
None
}
pub fn window_y(&self, window: &W::Id) -> Option<i32> {
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@@ -810,7 +738,7 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn activate_window(&mut self, window: &W) {
pub fn activate_window(&mut self, window: &W::Id) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
@@ -932,6 +860,27 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn with_windows_mut(&mut self, mut f: impl FnMut(&mut W, Option<&Output>)) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for win in ws.windows_mut() {
f(win, Some(&mon.output));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
for win in ws.windows_mut() {
f(win, None);
}
}
}
}
}
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal {
monitors,
@@ -1165,6 +1114,20 @@ impl<W: LayoutElement> Layout<W> {
monitor.switch_workspace(idx);
}
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.switch_workspace_auto_back_and_forth(idx);
}
pub fn switch_workspace_previous(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.switch_workspace_previous();
}
pub fn consume_into_column(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
@@ -1221,8 +1184,12 @@ impl<W: LayoutElement> Layout<W> {
#[cfg(test)]
fn verify_invariants(&self) {
use std::collections::HashSet;
use crate::layout::monitor::WorkspaceSwitch;
let mut seen_workspace_id = HashSet::new();
let (monitors, &primary_idx, &active_monitor_idx) = match &self.monitor_set {
MonitorSet::Normal {
monitors,
@@ -1241,6 +1208,11 @@ impl<W: LayoutElement> Layout<W> {
"workspace options must be synchronized with layout"
);
assert!(
seen_workspace_id.insert(workspace.id()),
"workspace id must be unique"
);
workspace.verify_invariants();
}
@@ -1325,6 +1297,11 @@ impl<W: LayoutElement> Layout<W> {
"workspace options must be synchronized with layout"
);
assert!(
seen_workspace_id.insert(workspace.id()),
"workspace id must be unique"
);
workspace.verify_invariants();
}
}
@@ -1465,7 +1442,7 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn move_window_to_output(&mut self, window: W, output: &Output) {
pub fn move_window_to_output(&mut self, window: &W::Id, output: &Output) {
let mut width = None;
let mut is_full_width = false;
@@ -1473,7 +1450,7 @@ impl<W: LayoutElement> Layout<W> {
for mon in &*monitors {
for ws in &mon.workspaces {
for col in &ws.columns {
if col.contains(&window) {
if col.contains(window) {
width = Some(col.width);
is_full_width = col.is_full_width;
break;
@@ -1485,7 +1462,7 @@ impl<W: LayoutElement> Layout<W> {
let Some(width) = width else { return };
self.remove_window(&window);
let window = self.remove_window(window).unwrap();
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
let new_idx = monitors
@@ -1529,6 +1506,8 @@ impl<W: LayoutElement> Layout<W> {
.unwrap();
let target = &mut monitors[target_idx];
target.previous_workspace_id = Some(target.workspaces[target.active_workspace_idx].id());
// 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);
@@ -1540,7 +1519,7 @@ impl<W: LayoutElement> Layout<W> {
*active_monitor_idx = target_idx;
}
pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@@ -1563,7 +1542,7 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn toggle_fullscreen(&mut self, window: &W) {
pub fn toggle_fullscreen(&mut self, window: &W::Id) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@@ -1716,14 +1695,14 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_workspace_up();
}
pub fn start_open_animation_for_window(&mut self, window: &W) {
pub fn start_open_animation_for_window(&mut self, window: &W::Id) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for col in &mut ws.columns {
for tile in &mut col.tiles {
if tile.window() == window {
if tile.window().id() == window {
tile.start_open_animation();
return;
}
@@ -1734,13 +1713,11 @@ impl<W: LayoutElement> Layout<W> {
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
for col in &mut ws.columns {
for tile in &mut col.tiles {
if tile.window() == window {
tile.start_open_animation();
return;
}
for col in &mut ws.columns {
for tile in &mut col.tiles {
if tile.window().id() == window {
tile.start_open_animation();
return;
}
}
}
@@ -1748,11 +1725,9 @@ impl<W: LayoutElement> Layout<W> {
}
}
}
}
impl Layout<Window> {
pub fn refresh(&mut self) {
let _span = tracy_client::span!("MonitorSet::refresh");
let _span = tracy_client::span!("Layout::refresh");
match &mut self.monitor_set {
MonitorSet::Normal {
@@ -1795,6 +1770,7 @@ mod tests {
use proptest::prelude::*;
use proptest_derive::Arbitrary;
use smithay::output::{Mode, PhysicalProperties, Subpixel};
use smithay::utils::Rectangle;
use super::*;
@@ -1859,13 +1835,13 @@ mod tests {
}
}
impl PartialEq for TestWindow {
fn eq(&self, other: &Self) -> bool {
self.0.id == other.0.id
}
}
impl LayoutElement for TestWindow {
type Id = usize;
fn id(&self) -> &Self::Id {
&self.0.id
}
fn size(&self) -> Size<i32, Logical> {
self.0.bbox.get().size
}
@@ -1883,6 +1859,8 @@ mod tests {
_renderer: &mut R,
_location: Point<i32, Logical>,
_scale: Scale<f64>,
_alpha: f32,
_target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
vec![]
}
@@ -1920,6 +1898,12 @@ mod tests {
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_activated(&mut self, _active: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn send_pending_configure(&self) {}
fn is_fullscreen(&self) -> bool {
false
}
@@ -1927,6 +1911,13 @@ mod tests {
fn is_pending_fullscreen(&self) -> bool {
self.0.pending_fullscreen.get()
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
}
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
@@ -2017,6 +2008,8 @@ mod tests {
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[proptest(strategy = "0..=4usize")] usize),
FocusWorkspaceAutoBackAndForth(#[proptest(strategy = "0..=4usize")] usize),
FocusWorkspacePrevious,
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
@@ -2176,24 +2169,14 @@ mod tests {
return;
}
let right_of_win = TestWindow::new(
right_of_id,
Rectangle::default(),
Size::default(),
Size::default(),
);
let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1);
layout.add_window_right_of(&right_of_win, win, None, false);
layout.add_window_right_of(&right_of_id, win, None, false);
}
Op::CloseWindow(id) => {
let dummy =
TestWindow::new(id, Rectangle::default(), Size::default(), Size::default());
layout.remove_window(&dummy);
layout.remove_window(&id);
}
Op::FullscreenWindow(id) => {
let dummy =
TestWindow::new(id, Rectangle::default(), Size::default(), Size::default());
layout.toggle_fullscreen(&dummy);
layout.toggle_fullscreen(&id);
}
Op::FocusColumnLeft => layout.focus_left(),
Op::FocusColumnRight => layout.focus_right(),
@@ -2219,6 +2202,10 @@ mod tests {
Op::FocusWorkspaceDown => layout.switch_workspace_down(),
Op::FocusWorkspaceUp => layout.switch_workspace_up(),
Op::FocusWorkspace(idx) => layout.switch_workspace(idx),
Op::FocusWorkspaceAutoBackAndForth(idx) => {
layout.switch_workspace_auto_back_and_forth(idx)
}
Op::FocusWorkspacePrevious => layout.switch_workspace_previous(),
Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(),
Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(),
Op::MoveWindowToWorkspace(idx) => layout.move_to_workspace(idx),
@@ -2248,7 +2235,7 @@ mod tests {
Op::SetColumnWidth(change) => layout.set_column_width(change),
Op::SetWindowHeight(change) => layout.set_window_height(change),
Op::Communicate(id) => {
let mut window = None;
let mut update = false;
match &mut layout.monitor_set {
MonitorSet::Normal { monitors, .. } => {
'outer: for mon in monitors {
@@ -2256,7 +2243,7 @@ mod tests {
for win in ws.windows() {
if win.0.id == id {
if win.communicate() {
window = Some(win.clone());
update = true;
}
break 'outer;
}
@@ -2269,7 +2256,7 @@ mod tests {
for win in ws.windows() {
if win.0.id == id {
if win.communicate() {
window = Some(win.clone());
update = true;
}
break 'outer;
}
@@ -2278,8 +2265,8 @@ mod tests {
}
}
if let Some(win) = window {
layout.update_window(&win);
if update {
layout.update_window(&id);
}
}
Op::MoveWorkspaceToOutput(id) => {
+69 -5
View File
@@ -10,11 +10,13 @@ use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use super::workspace::{
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::rubber_band::RubberBand;
use crate::swipe_tracker::SwipeTracker;
use crate::utils::output_size;
@@ -35,6 +37,8 @@ pub struct Monitor<W: LayoutElement> {
pub workspaces: Vec<Workspace<W>>,
/// Index of the currently active workspace.
pub active_workspace_idx: usize,
/// ID of the previously active workspace.
pub previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
pub workspace_switch: Option<WorkspaceSwitch>,
/// Configurable properties of the layout.
@@ -67,6 +71,13 @@ impl WorkspaceSwitch {
}
}
pub fn target_idx(&self) -> f64 {
match self {
WorkspaceSwitch::Animation(anim) => anim.to(),
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
}
}
/// Returns `true` if the workspace switch is [`Animation`].
///
/// [`Animation`]: WorkspaceSwitch::Animation
@@ -82,6 +93,7 @@ impl<W: LayoutElement> Monitor<W> {
output,
workspaces,
active_workspace_idx: 0,
previous_workspace_id: None,
workspace_switch: None,
options,
}
@@ -107,6 +119,8 @@ impl<W: LayoutElement> Monitor<W> {
.map(|s| s.current_idx())
.unwrap_or(self.active_workspace_idx as f64);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
self.active_workspace_idx = idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
@@ -146,7 +160,7 @@ impl<W: LayoutElement> Monitor<W> {
pub fn add_window_right_of(
&mut self,
right_of: &W,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
@@ -454,6 +468,11 @@ impl<W: LayoutElement> Monitor<W> {
));
}
fn previous_workspace_idx(&self) -> Option<usize> {
let id = self.previous_workspace_id?;
self.workspaces.iter().position(|w| w.id() == id)
}
pub fn switch_workspace(&mut self, idx: usize) {
self.activate_workspace(min(idx, self.workspaces.len() - 1));
// Don't animate this action.
@@ -462,6 +481,24 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
let idx = min(idx, self.workspaces.len() - 1);
if idx == self.active_workspace_idx {
if let Some(prev_idx) = self.previous_workspace_idx() {
self.switch_workspace(prev_idx);
}
} else {
self.switch_workspace(idx);
}
}
pub fn switch_workspace_previous(&mut self) {
if let Some(idx) = self.previous_workspace_idx() {
self.switch_workspace(idx);
}
}
pub fn consume_into_column(&mut self) {
self.active_workspace().consume_into_column();
}
@@ -557,8 +594,10 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.previous_workspace_id = previous_workspace_id;
self.clean_up_workspaces();
}
@@ -577,12 +616,33 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.previous_workspace_id = previous_workspace_id;
self.clean_up_workspaces();
}
/// Returns the geometry of the active tile relative to and clamped to the output.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<i32, Logical>> {
let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?;
if let Some(switch) = &self.workspace_switch {
let size = output_size(&self.output);
let offset = switch.target_idx() - self.active_workspace_idx as f64;
let offset = (offset * size.h as f64).round() as i32;
let clip_rect = Rectangle::from_loc_and_size((0, -offset), size);
rect = rect.intersection(clip_rect)?;
}
Some(rect)
}
pub fn window_under(
&self,
pos_within_output: Point<f64, Logical>,
@@ -641,6 +701,7 @@ impl<W: LayoutElement> Monitor<W> {
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
) -> Vec<MonitorRenderElement<R>> {
let _span = tracy_client::span!("Monitor::render_elements");
@@ -663,7 +724,7 @@ impl<W: LayoutElement> Monitor<W> {
let after_idx = after_idx as usize;
let after = if after_idx < self.workspaces.len() {
let after = self.workspaces[after_idx].render_elements(renderer);
let after = self.workspaces[after_idx].render_elements(renderer, target);
let after = after.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
@@ -693,7 +754,7 @@ impl<W: LayoutElement> Monitor<W> {
};
let before_idx = before_idx as usize;
let before = self.workspaces[before_idx].render_elements(renderer);
let before = self.workspaces[before_idx].render_elements(renderer, target);
let before = before.into_iter().filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
@@ -711,7 +772,8 @@ impl<W: LayoutElement> Monitor<W> {
before.chain(after.into_iter().flatten()).collect()
}
None => {
let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
let elements =
self.workspaces[self.active_workspace_idx].render_elements(renderer, target);
elements
.into_iter()
.filter_map(|elem| {
@@ -807,6 +869,8 @@ impl<W: LayoutElement> Monitor<W> {
gesture.center_idx as f64 + current_pos,
);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
self.active_workspace_idx = new_idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
gesture.current_idx,
+40 -7
View File
@@ -13,6 +13,7 @@ use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::offscreen::OffscreenRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -85,11 +86,22 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
let draw_border_with_background = self
.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
self.border
.update(self.window.size(), self.window.has_ssd());
.update(self.window.size(), !draw_border_with_background);
self.border.set_active(is_active);
self.focus_ring.update(self.tile_size(), self.has_ssd());
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
false
} else {
draw_border_with_background
};
self.focus_ring
.update(self.tile_size(), !draw_focus_ring_with_background);
self.focus_ring.set_active(is_active);
match &mut self.open_animation {
@@ -121,6 +133,10 @@ impl<W: LayoutElement> Tile<W> {
&self.window
}
pub fn window_mut(&mut self) -> &mut W {
&mut self.window
}
pub fn into_window(self) -> W {
self.window
}
@@ -291,8 +307,15 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn has_ssd(&self) -> bool {
self.effective_border_width().is_some() || self.window.has_ssd()
pub fn draw_border_with_background(&self) -> bool {
if self.effective_border_width().is_some() {
return false;
}
self.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd())
}
fn render_inner<R: NiriRenderer>(
@@ -302,10 +325,17 @@ impl<W: LayoutElement> Tile<W> {
scale: Scale<f64>,
view_size: Size<i32, Logical>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
let alpha = if self.is_fullscreen {
1.
} else {
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
};
let rv = self
.window
.render(renderer, location + self.window_loc(), scale)
.render(renderer, location + self.window_loc(), scale, alpha, target)
.into_iter()
.map(Into::into);
@@ -348,10 +378,12 @@ impl<W: LayoutElement> Tile<W> {
scale: Scale<f64>,
view_size: Size<i32, Logical>,
focus_ring: bool,
target: RenderTarget,
) -> impl Iterator<Item = TileRenderElement<R>> {
if let Some(anim) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
let elements =
self.render_inner(renderer, location, scale, view_size, focus_ring, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
let elem = OffscreenRenderElement::new(
@@ -379,7 +411,8 @@ impl<W: LayoutElement> Tile<W> {
} else {
self.window().set_offscreen_element_id(None);
let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
let elements =
self.render_inner(renderer, location, scale, view_size, focus_ring, target);
None.into_iter().chain(Some(elements).into_iter().flatten())
}
}
+179 -56
View File
@@ -5,19 +5,21 @@ use std::time::Duration;
use niri_config::{CenterFocusedColumn, PresetWidth, Struts};
use niri_ipc::SizeChange;
use smithay::desktop::space::SpaceElement;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::wayland::compositor::send_surface_state;
use super::tile::{Tile, TileRenderElement};
use super::{LayoutElement, Options};
use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::swipe_tracker::SwipeTracker;
use crate::utils::id::IdCounter;
use crate::utils::output_size;
/// Amount of touchpad movement to scroll the view for the width of one working area.
@@ -70,15 +72,31 @@ pub struct Workspace<W: LayoutElement> {
/// Since we only create-and-activate columns immediately to the right of the active column (in
/// contrast to tabs in Firefox, for example), we can track this as a bool, rather than an
/// index of the previous column to activate.
activate_prev_column_on_removal: bool,
///
/// The value is the view offset that the previous column had before, to restore it.
activate_prev_column_on_removal: Option<i32>,
/// Configurable properties of the layout.
pub options: Rc<Options>,
/// Unique ID of this workspace.
id: WorkspaceId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputId(String);
static WORKSPACE_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WorkspaceId(u32);
impl WorkspaceId {
fn next() -> WorkspaceId {
WorkspaceId(WORKSPACE_ID_COUNTER.next())
}
}
niri_render_elements! {
WorkspaceRenderElement<R> => {
Tile = TileRenderElement<R>,
@@ -96,6 +114,8 @@ struct ViewGesture {
current_view_offset: f64,
tracker: SwipeTracker,
delta_from_tracker: f64,
// The view offset we'll use if needed for activate_prev_column_on_removal.
static_view_offset: i32,
}
/// Width of a column.
@@ -177,6 +197,15 @@ impl OutputId {
}
}
impl ViewOffsetAdjustment {
pub fn target_view_offset(&self) -> f64 {
match self {
ViewOffsetAdjustment::Animation(anim) => anim.to(),
ViewOffsetAdjustment::Gesture(gesture) => gesture.current_view_offset,
}
}
}
impl ColumnWidth {
fn resolve(self, options: &Options, view_width: i32) -> i32 {
match self {
@@ -210,8 +239,9 @@ impl<W: LayoutElement> Workspace<W> {
active_column_idx: 0,
view_offset: 0,
view_offset_adj: None,
activate_prev_column_on_removal: false,
activate_prev_column_on_removal: None,
options,
id: WorkspaceId::next(),
}
}
@@ -225,11 +255,16 @@ impl<W: LayoutElement> Workspace<W> {
active_column_idx: 0,
view_offset: 0,
view_offset_adj: None,
activate_prev_column_on_removal: false,
activate_prev_column_on_removal: None,
options,
id: WorkspaceId::next(),
}
}
pub fn id(&self) -> WorkspaceId {
self.id
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
anim.set_current_time(current_time);
@@ -266,6 +301,13 @@ impl<W: LayoutElement> Workspace<W> {
.map(Tile::window)
}
pub fn windows_mut(&mut self) -> impl Iterator<Item = &mut W> + '_ {
self.columns
.iter_mut()
.flat_map(|col| col.tiles.iter_mut())
.map(Tile::window_mut)
}
pub fn set_output(&mut self, output: Option<Output>) {
if self.output == output {
return;
@@ -374,7 +416,11 @@ impl<W: LayoutElement> Workspace<W> {
pub fn configure_new_window(&self, window: &Window, width: Option<ColumnWidth>) {
if let Some(output) = self.output.as_ref() {
set_preferred_scale_transform(window, output);
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
window.with_surfaces(|surface, data| {
send_surface_state(surface, data, scale, transform);
});
}
window
@@ -536,21 +582,25 @@ impl<W: LayoutElement> Workspace<W> {
self.active_column_idx = idx;
// A different column was activated; reset the flag.
self.activate_prev_column_on_removal = false;
self.activate_prev_column_on_removal = None;
}
pub fn has_windows(&self) -> bool {
self.windows().next().is_some()
}
pub fn has_window(&self, window: &W) -> bool {
self.windows().any(|win| win == window)
pub fn has_window(&self, window: &W::Id) -> bool {
self.windows().any(|win| win.id() == window)
}
pub fn find_wl_surface(&self, wl_surface: &WlSurface) -> Option<&W> {
self.windows().find(|win| win.is_wl_surface(wl_surface))
}
pub fn find_wl_surface_mut(&mut self, wl_surface: &WlSurface) -> Option<&mut W> {
self.windows_mut().find(|win| win.is_wl_surface(wl_surface))
}
/// Computes the X position of the windows in the given column, in logical coordinates.
fn column_x(&self, column_idx: usize) -> i32 {
let mut x = 0;
@@ -615,14 +665,16 @@ impl<W: LayoutElement> Workspace<W> {
self.view_offset_adj = None;
}
let prev_offset = (!was_empty).then(|| self.static_view_offset());
self.activate_column(idx);
self.activate_prev_column_on_removal = true;
self.activate_prev_column_on_removal = prev_offset;
}
}
pub fn add_window_right_of(
&mut self,
right_of: &W,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
@@ -648,8 +700,9 @@ impl<W: LayoutElement> Workspace<W> {
// Activate the new window if right_of was active.
if self.active_column_idx == right_of_idx {
let prev_offset = self.static_view_offset();
self.activate_column(idx);
self.activate_prev_column_on_removal = true;
self.activate_prev_column_on_removal = Some(prev_offset);
} else if idx <= self.active_column_idx {
self.active_column_idx += 1;
}
@@ -687,8 +740,10 @@ impl<W: LayoutElement> Workspace<W> {
self.view_offset_adj = None;
}
let prev_offset = (!was_empty).then(|| self.static_view_offset());
self.activate_column(idx);
self.activate_prev_column_on_removal = true;
self.activate_prev_column_on_removal = prev_offset;
}
}
@@ -705,7 +760,7 @@ impl<W: LayoutElement> Workspace<W> {
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;
self.activate_prev_column_on_removal = None;
}
// FIXME: activate_column below computes current view position to compute the new view
@@ -716,13 +771,25 @@ impl<W: LayoutElement> Workspace<W> {
return window;
}
if self.active_column_idx > column_idx
|| (self.active_column_idx == column_idx && self.activate_prev_column_on_removal)
{
if column_idx < self.active_column_idx {
// 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));
self.activate_column(self.active_column_idx - 1);
} else if column_idx == self.active_column_idx
&& self.activate_prev_column_on_removal.is_some()
{
// The active column was removed, and we needed to activate the previous column.
if 0 < column_idx {
let prev_offset = self.activate_prev_column_on_removal.unwrap();
self.activate_column(self.active_column_idx - 1);
// Restore the view offset but make sure to scroll the view in case the
// previous window had resized.
let current_x = self.view_pos();
self.animate_view_offset(current_x, self.active_column_idx, prev_offset);
self.animate_view_offset_to_column(current_x, self.active_column_idx, None);
}
} else {
self.activate_column(min(self.active_column_idx, self.columns.len() - 1));
}
@@ -748,7 +815,7 @@ impl<W: LayoutElement> Workspace<W> {
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;
self.activate_prev_column_on_removal = None;
}
// FIXME: activate_column below computes current view position to compute the new view
@@ -758,13 +825,25 @@ impl<W: LayoutElement> Workspace<W> {
return column;
}
if self.active_column_idx > column_idx
|| (self.active_column_idx == column_idx && self.activate_prev_column_on_removal)
{
if column_idx < self.active_column_idx {
// 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));
self.activate_column(self.active_column_idx - 1);
} else if column_idx == self.active_column_idx
&& self.activate_prev_column_on_removal.is_some()
{
// The active column was removed, and we needed to activate the previous column.
if 0 < column_idx {
let prev_offset = self.activate_prev_column_on_removal.unwrap();
self.activate_column(self.active_column_idx - 1);
// Restore the view offset but make sure to scroll the view in case the
// previous window had resized.
let current_x = self.view_pos();
self.animate_view_offset(current_x, self.active_column_idx, prev_offset);
self.animate_view_offset_to_column(current_x, self.active_column_idx, None);
}
} else {
self.activate_column(min(self.active_column_idx, self.columns.len() - 1));
}
@@ -772,7 +851,7 @@ impl<W: LayoutElement> Workspace<W> {
column
}
pub fn remove_window(&mut self, window: &W) {
pub fn remove_window(&mut self, window: &W::Id) -> W {
let column_idx = self
.columns
.iter()
@@ -781,10 +860,10 @@ impl<W: LayoutElement> Workspace<W> {
let column = &self.columns[column_idx];
let window_idx = column.position(window).unwrap();
self.remove_window_by_idx(column_idx, window_idx);
self.remove_window_by_idx(column_idx, window_idx)
}
pub fn update_window(&mut self, window: &W) {
pub fn update_window(&mut self, window: &W::Id) {
let (idx, column) = self
.columns
.iter_mut()
@@ -806,7 +885,7 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn activate_window(&mut self, window: &W) {
pub fn activate_window(&mut self, window: &W::Id) {
let column_idx = self
.columns
.iter()
@@ -1040,6 +1119,18 @@ impl<W: LayoutElement> Workspace<W> {
self.column_x(self.active_column_idx) + self.view_offset
}
/// Returns a view offset value suitable for saving and later restoration.
///
/// This means that it shouldn't return an in-progress animation or gesture value.
fn static_view_offset(&self) -> i32 {
match &self.view_offset_adj {
// For animations we can return the final value.
Some(ViewOffsetAdjustment::Animation(anim)) => anim.to().round() as i32,
Some(ViewOffsetAdjustment::Gesture(gesture)) => gesture.static_view_offset,
_ => self.view_offset,
}
}
fn tiles_in_render_order(&self) -> impl Iterator<Item = (&'_ Tile<W>, Point<i32, Logical>)> {
let view_pos = self.visual_column_x(self.active_column_idx) + self.view_offset;
@@ -1079,6 +1170,31 @@ impl<W: LayoutElement> Workspace<W> {
first.chain(rest)
}
fn active_column_ref(&self) -> Option<&Column<W>> {
if self.columns.is_empty() {
return None;
}
Some(&self.columns[self.active_column_idx])
}
/// Returns the geometry of the active tile relative to and clamped to the view.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<i32, Logical>> {
let col = self.active_column_ref()?;
let view_pos = self
.view_offset_adj
.as_ref()
.map_or(self.view_offset, |adj| adj.target_view_offset() as i32);
let tile_pos = Point::from((-view_pos, col.tile_y(col.active_tile_idx)));
let tile_size = col.active_tile_ref().tile_size();
let tile_rect = Rectangle::from_loc_and_size(tile_pos, tile_size);
let view = Rectangle::from_loc_and_size((0, 0), self.view_size);
view.intersection(tile_rect)
}
pub fn window_under(
&self,
pos: Point<f64, Logical>,
@@ -1133,7 +1249,7 @@ impl<W: LayoutElement> Workspace<W> {
self.columns[self.active_column_idx].set_window_height(change);
}
pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
let (mut col_idx, tile_idx) = self
.columns
.iter()
@@ -1175,7 +1291,7 @@ impl<W: LayoutElement> Workspace<W> {
col.set_fullscreen(is_fullscreen);
}
pub fn toggle_fullscreen(&mut self, window: &W) {
pub fn toggle_fullscreen(&mut self, window: &W::Id) {
let col = self
.columns
.iter_mut()
@@ -1201,6 +1317,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
) -> Vec<WorkspaceRenderElement<R>> {
if self.columns.is_empty() {
return vec![];
@@ -1223,8 +1340,15 @@ impl<W: LayoutElement> Workspace<W> {
first = false;
rv.extend(
tile.render(renderer, tile_pos, output_scale, self.view_size, focus_ring)
.map(Into::into),
tile.render(
renderer,
tile_pos,
output_scale,
self.view_size,
focus_ring,
target,
)
.map(Into::into),
);
}
@@ -1240,6 +1364,7 @@ impl<W: LayoutElement> Workspace<W> {
current_view_offset: self.view_offset as f64,
tracker: SwipeTracker::new(),
delta_from_tracker: self.view_offset as f64,
static_view_offset: self.static_view_offset(),
};
self.view_offset_adj = Some(ViewOffsetAdjustment::Gesture(gesture));
}
@@ -1433,29 +1558,20 @@ impl<W: LayoutElement> Workspace<W> {
true
}
}
impl Workspace<Window> {
pub fn refresh(&self, is_active: bool) {
pub fn refresh(&mut self, is_active: bool) {
let bounds = self.toplevel_bounds();
for (col_idx, col) in self.columns.iter().enumerate() {
for (tile_idx, tile) in col.tiles.iter().enumerate() {
let win = tile.window();
for (col_idx, col) in self.columns.iter_mut().enumerate() {
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
let win = tile.window_mut();
let active = is_active
&& self.active_column_idx == col_idx
&& col.active_tile_idx == tile_idx;
win.set_activated(active);
win.toplevel()
.expect("no x11 support")
.with_pending_state(|state| {
state.bounds = Some(bounds);
});
win.toplevel()
.expect("no x11 support")
.send_pending_configure();
win.set_bounds(bounds);
win.send_pending_configure();
win.refresh();
}
}
@@ -1553,18 +1669,21 @@ impl<W: LayoutElement> Column<W> {
self.tiles.iter().any(Tile::are_animations_ongoing)
}
pub fn contains(&self, window: &W) -> bool {
self.tiles.iter().map(Tile::window).any(|win| win == window)
}
pub fn position(&self, window: &W) -> Option<usize> {
pub fn contains(&self, window: &W::Id) -> bool {
self.tiles
.iter()
.map(Tile::window)
.position(|win| win == window)
.any(|win| win.id() == window)
}
fn activate_window(&mut self, window: &W) {
pub fn position(&self, window: &W::Id) -> Option<usize> {
self.tiles
.iter()
.map(Tile::window)
.position(|win| win.id() == window)
}
fn activate_window(&mut self, window: &W::Id) {
let idx = self.position(window).unwrap();
self.active_tile_idx = idx;
}
@@ -1577,11 +1696,11 @@ impl<W: LayoutElement> Column<W> {
self.update_tile_sizes();
}
fn update_window(&mut self, window: &W) {
fn update_window(&mut self, window: &W::Id) {
let tile = self
.tiles
.iter_mut()
.find(|tile| tile.window() == window)
.find(|tile| tile.window().id() == window)
.unwrap();
tile.update_window();
}
@@ -1964,6 +2083,10 @@ impl<W: LayoutElement> Column<W> {
pos
})
}
fn active_tile_ref(&self) -> &Tile<W> {
&self.tiles[self.active_tile_idx]
}
}
fn compute_new_view_offset(
+1
View File
@@ -16,6 +16,7 @@ pub mod niri;
pub mod protocols;
pub mod render_helpers;
pub mod rubber_band;
pub mod scroll_tracker;
pub mod swipe_tracker;
pub mod ui;
pub mod utils;
+683 -185
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -95,8 +95,8 @@ pub fn refresh(state: &mut State) {
// 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().expect("no x11 support").wl_surface();
state.niri.layout.with_windows(|mapped, output| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
@@ -106,8 +106,8 @@ pub fn refresh(state: &mut State) {
.lock()
.unwrap();
if state.niri.keyboard_focus.as_ref() == Some(wl_surface) {
focused = Some((window.clone(), output.cloned()));
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((mapped.window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
}
@@ -172,7 +172,7 @@ fn refresh_toplevel(
let mut new_title = None;
if data.title != role.title {
data.title = role.title.clone();
data.title.clone_from(&role.title);
new_title = role.title.as_deref();
if new_title.is_none() {
@@ -182,7 +182,7 @@ fn refresh_toplevel(
let mut new_app_id = None;
if data.app_id != role.app_id {
data.app_id = role.app_id.clone();
data.app_id.clone_from(&role.app_id);
new_app_id = role.app_id.as_deref();
if new_app_id.is_none() {
+246
View File
@@ -0,0 +1,246 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use smithay::output::Output;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use wayland_protocols_wlr::gamma_control::v1::server::{
zwlr_gamma_control_manager_v1, zwlr_gamma_control_v1,
};
use zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1;
use zwlr_gamma_control_v1::ZwlrGammaControlV1;
const VERSION: u32 = 1;
pub struct GammaControlManagerState {
// Active gamma controls only. Failed ones are removed.
gamma_controls: HashMap<Output, ZwlrGammaControlV1>,
}
pub struct GammaControlManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
pub trait GammaControlHandler {
fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState;
fn get_gamma_size(&mut self, output: &Output) -> Option<u32>;
fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> Option<()>;
}
pub struct GammaControlState {
gamma_size: u32,
}
impl GammaControlManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>,
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = GammaControlManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrGammaControlManagerV1, _>(VERSION, global_data);
Self {
gamma_controls: HashMap::new(),
}
}
pub fn output_removed(&mut self, output: &Output) {
if let Some(gamma_control) = self.gamma_controls.remove(output) {
gamma_control.failed();
}
}
}
impl<D> GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData, D>
for GammaControlManagerState
where
D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>,
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_handle: &DisplayHandle,
_client: &Client,
manager: New<ZwlrGammaControlManagerV1>,
_manager_state: &GammaControlManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &GammaControlManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrGammaControlManagerV1, (), D> for GammaControlManagerState
where
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
_resource: &ZwlrGammaControlManagerV1,
request: <ZwlrGammaControlManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_gamma_control_manager_v1::Request::GetGammaControl { id, output } => {
if let Some(output) = Output::from_resource(&output) {
// We borrow state in the middle.
#[allow(clippy::map_entry)]
if !state
.gamma_control_manager_state()
.gamma_controls
.contains_key(&output)
{
if let Some(gamma_size) = state.get_gamma_size(&output) {
let zwlr_gamma_control =
data_init.init(id, GammaControlState { gamma_size });
zwlr_gamma_control.gamma_size(gamma_size);
state
.gamma_control_manager_state()
.gamma_controls
.insert(output, zwlr_gamma_control);
return;
}
}
}
data_init
.init(id, GammaControlState { gamma_size: 0 })
.failed();
}
zwlr_gamma_control_manager_v1::Request::Destroy => (),
_ => unreachable!(),
}
}
}
impl<D> Dispatch<ZwlrGammaControlV1, GammaControlState, D> for GammaControlManagerState
where
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
D: GammaControlHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrGammaControlV1,
request: <ZwlrGammaControlV1 as Resource>::Request,
data: &GammaControlState,
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_gamma_control_v1::Request::SetGamma { fd } => {
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
let Some((output, _)) = gamma_controls.iter().find(|(_, x)| *x == resource) else {
return;
};
let output = output.clone();
trace!("setting gamma for output {}", output.name());
// Start with a u16 slice so it's aligned correctly.
let mut gamma = vec![0u16; data.gamma_size as usize * 3];
let buf = bytemuck::cast_slice_mut(&mut gamma);
let mut file = File::from(fd);
{
let _span = tracy_client::span!("read gamma from fd");
if let Err(err) = file.read_exact(buf) {
warn!("failed to read gamma data: {err:?}");
resource.failed();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
return;
}
// Verify that there's no more data.
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
{
match file.read(&mut [0]) {
Ok(0) => (),
Ok(_) => {
warn!("gamma data is too large");
resource.failed();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
return;
}
Err(err) => {
warn!("error reading gamma data: {err:?}");
resource.failed();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
return;
}
}
}
}
if state.set_gamma(&output, Some(gamma)).is_none() {
resource.failed();
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
}
}
zwlr_gamma_control_v1::Request::Destroy => (),
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrGammaControlV1,
_data: &GammaControlState,
) {
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
let Some((output, _)) = gamma_controls.iter().find(|(_, x)| *x == resource) else {
return;
};
let output = output.clone();
gamma_controls.remove(&output);
let _ = state.set_gamma(&output, None);
}
}
#[macro_export]
macro_rules! delegate_gamma_control {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: $crate::protocols::gamma_control::GammaControlManagerGlobalData
] => $crate::protocols::gamma_control::GammaControlManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: ()
] => $crate::protocols::gamma_control::GammaControlManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_v1::ZwlrGammaControlV1: $crate::protocols::gamma_control::GammaControlState
] => $crate::protocols::gamma_control::GammaControlManagerState);
};
}
+1
View File
@@ -1,2 +1,3 @@
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod screencopy;
+11
View File
@@ -19,6 +19,17 @@ pub mod render_elements;
pub mod renderer;
pub mod shaders;
/// What we're rendering for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderTarget {
/// Rendering to display on screen.
Output,
/// Rendering for a screencast.
Screencast,
/// Rendering for any other screen capture.
ScreenCapture,
}
pub fn render_to_texture(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
+40
View File
@@ -0,0 +1,40 @@
pub struct ScrollTracker {
tick: f64,
last: f64,
acc: f64,
}
impl ScrollTracker {
#[allow(clippy::new_without_default)]
pub fn new(tick: i8) -> Self {
Self {
tick: f64::from(tick),
last: 0.,
acc: 0.,
}
}
pub fn accumulate(&mut self, amount: f64) -> i8 {
let changed_direction = (self.last > 0. && amount < 0.) || (self.last < 0. && amount > 0.);
if changed_direction {
self.acc = 0.
}
self.last = amount;
self.acc += amount;
let mut ticks = 0;
if self.acc.abs() >= self.tick {
let clamped = self.acc.clamp(-127. * self.tick, 127. * self.tick);
ticks = (clamped as i16 / self.tick as i16) as i8;
self.acc %= self.tick;
}
ticks
}
pub fn reset(&mut self) {
self.last = 0.;
self.acc = 0.;
}
}
+16 -2
View File
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use std::iter::zip;
use std::rc::Rc;
use niri_config::{Action, Config, Key, Modifiers};
use niri_config::{Action, Config, Key, Modifiers, Trigger};
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
use smithay::backend::renderer::element::memory::{
@@ -226,6 +226,8 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
// Only show binds with Mod or Super to filter out stuff like volume up/down.
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|| bind.key.modifiers.contains(Modifiers::SUPER))
// Also filter out wheel and touchpad scroll binds.
&& matches!(bind.key.trigger, Trigger::Keysym(_))
}) {
let action = &bind.action;
@@ -414,7 +416,19 @@ fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
if key.modifiers.contains(Modifiers::CTRL) {
name.push_str("Ctrl + ");
}
name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym)));
let pretty = match key.trigger {
Trigger::Keysym(keysym) => prettify_keysym_name(&keysym_get_name(keysym)),
Trigger::WheelScrollDown => String::from("Wheel Scroll Down"),
Trigger::WheelScrollUp => String::from("Wheel Scroll Up"),
Trigger::WheelScrollLeft => String::from("Wheel Scroll Left"),
Trigger::WheelScrollRight => String::from("Wheel Scroll Right"),
Trigger::TouchpadScrollDown => String::from("Touchpad Scroll Down"),
Trigger::TouchpadScrollUp => String::from("Touchpad Scroll Up"),
Trigger::TouchpadScrollLeft => String::from("Touchpad Scroll Left"),
Trigger::TouchpadScrollRight => String::from("Touchpad Scroll Right"),
};
name.push_str(&pretty);
name
}
+23 -15
View File
@@ -19,6 +19,7 @@ use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::RenderTarget;
const BORDER: i32 = 2;
@@ -42,8 +43,9 @@ pub struct OutputData {
size: Size<i32, Physical>,
scale: i32,
transform: Transform,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
// Output, screencast, screen capture.
texture: [GlesTexture; 3],
texture_buffer: [TextureBuffer<GlesTexture>; 3],
buffers: [SolidColorBuffer; 8],
locations: [Point<i32, Physical>; 8],
}
@@ -65,7 +67,8 @@ impl ScreenshotUi {
pub fn open(
&mut self,
renderer: &GlesRenderer,
screenshots: HashMap<Output, GlesTexture>,
// Output, screencast, screen capture.
screenshots: HashMap<Output, [GlesTexture; 3]>,
default_output: Output,
) -> bool {
if screenshots.is_empty() {
@@ -110,13 +113,9 @@ impl ScreenshotUi {
let output_mode = output.current_mode().unwrap();
let size = transform.transform_size(output_mode.size);
let scale = output.current_scale().integer_scale();
let texture_buffer = TextureBuffer::from_texture(
renderer,
texture.clone(),
scale,
Transform::Normal,
None,
);
let texture_buffer = texture.clone().map(|texture| {
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None)
});
let buffers = [
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
@@ -243,7 +242,11 @@ impl ScreenshotUi {
}
}
pub fn render_output(&self, output: &Output) -> ArrayVec<ScreenshotUiRenderElement, 9> {
pub fn render_output(
&self,
output: &Output,
target: RenderTarget,
) -> ArrayVec<ScreenshotUiRenderElement, 9> {
let _span = tracy_client::span!("ScreenshotUi::render_output");
let Self::Open { output_data, .. } = self else {
@@ -269,10 +272,15 @@ impl ScreenshotUi {
}));
// The screenshot itself goes last.
let index = match target {
RenderTarget::Output => 0,
RenderTarget::Screencast => 1,
RenderTarget::ScreenCapture => 2,
};
elements.push(
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
(0., 0.),
&output_data.texture_buffer,
&output_data.texture_buffer[index],
None,
None,
None,
@@ -307,7 +315,7 @@ impl ScreenshotUi {
.to_buffer(1, Transform::Normal, &data.size.to_logical(1));
let mapping = renderer
.copy_texture(&data.texture, buf_rect, Fourcc::Abgr8888)
.copy_texture(&data.texture[0], buf_rect, Fourcc::Abgr8888)
.context("error copying texture")?;
let copy = renderer
.map_texture(&mapping)
@@ -316,12 +324,12 @@ impl ScreenshotUi {
Ok((rect.size, copy.to_vec()))
}
pub fn action(&self, raw: Option<Keysym>, mods: ModifiersState) -> Option<Action> {
pub fn action(&self, raw: Keysym, mods: ModifiersState) -> Option<Action> {
if !matches!(self, Self::Open { .. }) {
return None;
}
action(raw?, mods)
action(raw, mods)
}
pub fn selection_output(&self) -> Option<&Output> {
+27
View File
@@ -0,0 +1,27 @@
use std::sync::atomic::{AtomicU32, Ordering};
/// Counter that returns unique IDs.
///
/// Under the hood it uses a `u32` that will eventually wrap around. When incrementing it once a
/// second, it will wrap around after about 136 years.
pub struct IdCounter {
value: AtomicU32,
}
impl IdCounter {
pub const fn new() -> Self {
Self {
value: AtomicU32::new(0),
}
}
pub fn next(&self) -> u32 {
self.value.fetch_add(1, Ordering::SeqCst)
}
}
impl Default for IdCounter {
fn default() -> Self {
Self::new()
}
}
+42 -5
View File
@@ -12,17 +12,14 @@ use git_version::git_version;
use niri_config::Config;
use smithay::output::Output;
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
use smithay::utils::{Logical, Point, Rectangle, Size};
use smithay::utils::{Logical, Point, Rectangle, Size, Transform};
pub mod id;
pub mod spawning;
pub mod watcher;
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
(t.0.clone(), t.1.clone())
}
pub fn version() -> String {
format!(
"{} ({})",
@@ -40,6 +37,10 @@ pub fn center(rect: Rectangle<i32, Logical>) -> Point<i32, Logical> {
rect.loc + rect.size.downscale(2).to_point()
}
pub fn center_f64(rect: Rectangle<f64, Logical>) -> Point<f64, Logical> {
rect.loc + rect.size.downscale(2.0).to_point()
}
pub fn output_size(output: &Output) -> Size<i32, Logical> {
let output_scale = output.current_scale().integer_scale();
let output_transform = output.current_transform();
@@ -50,6 +51,42 @@ pub fn output_size(output: &Output) -> Size<i32, Logical> {
.to_logical(output_scale)
}
pub fn logical_output(output: &Output) -> niri_ipc::LogicalOutput {
let loc = output.current_location();
let size = output_size(output);
let transform = match output.current_transform() {
Transform::Normal => niri_ipc::Transform::Normal,
Transform::_90 => niri_ipc::Transform::_90,
Transform::_180 => niri_ipc::Transform::_180,
Transform::_270 => niri_ipc::Transform::_270,
Transform::Flipped => niri_ipc::Transform::Flipped,
Transform::Flipped90 => niri_ipc::Transform::Flipped90,
Transform::Flipped180 => niri_ipc::Transform::Flipped180,
Transform::Flipped270 => niri_ipc::Transform::Flipped270,
};
niri_ipc::LogicalOutput {
x: loc.x,
y: loc.y,
width: size.w as u32,
height: size.h as u32,
scale: output.current_scale().fractional_scale(),
transform,
}
}
pub fn ipc_transform_to_smithay(transform: niri_ipc::Transform) -> Transform {
match transform {
niri_ipc::Transform::Normal => Transform::Normal,
niri_ipc::Transform::_90 => Transform::_90,
niri_ipc::Transform::_180 => Transform::_180,
niri_ipc::Transform::_270 => Transform::_270,
niri_ipc::Transform::Flipped => Transform::Flipped,
niri_ipc::Transform::Flipped90 => Transform::Flipped90,
niri_ipc::Transform::Flipped180 => Transform::Flipped180,
niri_ipc::Transform::Flipped270 => Transform::Flipped270,
}
}
pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> {
if let Ok(rest) = path.strip_prefix("~") {
let dirs = UserDirs::new().context("error retrieving home directory")?;
+268 -214
View File
@@ -1,17 +1,12 @@
use std::ffi::OsStr;
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, Stdio};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use std::{io, thread};
use libc::close_range;
use niri_config::Environment;
use smithay::reexports::rustix;
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
use crate::utils::expand_home;
@@ -81,123 +76,10 @@ fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl As
}
drop(env);
// When running as a systemd session, we want to put children into their own transient scopes
// in order to separate them from the niri process. This is helpful for example to prevent the
// OOM killer from taking down niri together with a misbehaving client.
//
// Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
// with a PID. Unfortunately, there seems to be a race in systemd where if the child exits at
// just the right time, the transient unit will be created but empty, so it will linger around
// forever.
//
// To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
// intermediate child we will send back the grandchild PID, and in niri we will create a
// transient scope with both our intermediate child and the grandchild PIDs set. Only then we
// will signal our intermediate child to exit. This way, even if the grandchild exits quickly,
// a non-empty scope will be created (with just our intermediate child), then cleaned up when
// our intermediate child exits.
// Make a pipe to receive the grandchild PID.
let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe to transfer child PID: {err:?}");
})
.ok()
.unzip();
// Make a pipe to wait in the intermediate child.
let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe for child to wait on: {err:?}");
})
.ok()
.unzip();
unsafe {
// The fds will be duplicated after a fork and closed on exec or exit automatically. Get
// the raw fd inside so that it's not closed any extra times.
let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
// Double-fork to avoid having to waitpid the child.
process.pre_exec(move || {
// Close FDs that we don't need. Especially important for the write ones to unblock the
// readers.
if let Some(fd) = pipe_pid_read_fd.take() {
close(fd);
}
if let Some(fd) = pipe_wait_write_fd.take() {
close(fd);
}
// Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
grandchild_pid => {
// Send back the PID.
if let Some(pipe) = pipe_pid_write {
let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
}
// Wait until the parent signals us to exit.
if let Some(pipe) = pipe_wait_read {
// We're going to exit afterwards. Close all other FDs to allow
// Command::spawn() to return in the parent process.
let raw = pipe.as_raw_fd() as u32;
let _ = close_range(0, raw - 1, 0);
let _ = close_range(raw + 1, !0, 0);
let _ = read_all(pipe, &mut [0]);
}
libc::_exit(0)
}
}
Ok(())
});
}
let mut child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return;
}
let Some(mut child) = do_spawn(command, process) else {
return;
};
drop(pipe_pid_write);
drop(pipe_wait_read);
// Wait for the grandchild PID.
if let Some(pipe) = pipe_pid_read {
let mut buf = [0; 4];
match read_all(pipe, &mut buf) {
Ok(()) => {
let pid = i32::from_ne_bytes(buf);
trace!("spawned PID: {pid}");
// Start a systemd scope for the grandchild.
#[cfg(feature = "systemd")]
if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) {
trace!("error starting systemd scope for spawned command: {err:?}");
}
}
Err(err) => {
warn!("error reading child PID: {err:?}");
}
}
}
// Signal the intermediate child to exit now that we're done trying to creating a systemd scope.
trace!("signaling child to exit");
drop(pipe_wait_write);
match child.wait() {
Ok(status) => {
if !status.success() {
@@ -210,115 +92,287 @@ fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl As
}
}
fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
let mut written = 0;
loop {
let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
#[cfg(not(feature = "systemd"))]
fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
unsafe {
// Double-fork to avoid having to waitpid the child.
process.pre_exec(move || {
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
_ => libc::_exit(0),
}
written += n;
if written == buf.len() {
return Ok(());
}
Ok(())
});
}
let child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return None;
}
};
Some(child)
}
fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
let mut start = 0;
loop {
let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
start += n;
if start == buf.len() {
return Ok(());
}
}
}
/// Puts a (newly spawned) pid into a transient systemd scope.
///
/// This separates the pid from the compositor scope, which for example prevents the OOM killer
/// from bringing down the compositor together with a misbehaving client.
#[cfg(feature = "systemd")]
fn start_systemd_scope(name: &OsStr, intermediate_pid: u32, child_pid: u32) -> anyhow::Result<()> {
use std::fmt::Write as _;
use std::os::unix::ffi::OsStrExt;
use std::sync::OnceLock;
use systemd::do_spawn;
use anyhow::Context;
use zbus::zvariant::{OwnedObjectPath, Value};
#[cfg(feature = "systemd")]
mod systemd {
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use crate::utils::IS_SYSTEMD_SERVICE;
use smithay::reexports::rustix;
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
// We only start transient scopes if we're a systemd service ourselves.
if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) {
return Ok(());
use super::*;
pub fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
use libc::close_range;
// When running as a systemd session, we want to put children into their own transient
// scopes in order to separate them from the niri process. This is helpful for
// example to prevent the OOM killer from taking down niri together with a
// misbehaving client.
//
// Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
// with a PID. Unfortunately, there seems to be a race in systemd where if the child exits
// at just the right time, the transient unit will be created but empty, so it will
// linger around forever.
//
// To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
// intermediate child we will send back the grandchild PID, and in niri we will create a
// transient scope with both our intermediate child and the grandchild PIDs set. Only then
// we will signal our intermediate child to exit. This way, even if the grandchild
// exits quickly, a non-empty scope will be created (with just our intermediate
// child), then cleaned up when our intermediate child exits.
// Make a pipe to receive the grandchild PID.
let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe to transfer child PID: {err:?}");
})
.ok()
.unzip();
// Make a pipe to wait in the intermediate child.
let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
.map_err(|err| {
warn!("error creating a pipe for child to wait on: {err:?}");
})
.ok()
.unzip();
unsafe {
// The fds will be duplicated after a fork and closed on exec or exit automatically. Get
// the raw fd inside so that it's not closed any extra times.
let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
// Double-fork to avoid having to waitpid the child.
process.pre_exec(move || {
// Close FDs that we don't need. Especially important for the write ones to unblock
// the readers.
if let Some(fd) = pipe_pid_read_fd.take() {
close(fd);
}
if let Some(fd) = pipe_wait_write_fd.take() {
close(fd);
}
// Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
grandchild_pid => {
// Send back the PID.
if let Some(pipe) = pipe_pid_write {
let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
}
// Wait until the parent signals us to exit.
if let Some(pipe) = pipe_wait_read {
// We're going to exit afterwards. Close all other FDs to allow
// Command::spawn() to return in the parent process.
let raw = pipe.as_raw_fd() as u32;
let _ = close_range(0, raw - 1, 0);
let _ = close_range(raw + 1, !0, 0);
let _ = read_all(pipe, &mut [0]);
}
libc::_exit(0)
}
}
Ok(())
});
}
let child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return None;
}
};
drop(pipe_pid_write);
drop(pipe_wait_read);
// Wait for the grandchild PID.
if let Some(pipe) = pipe_pid_read {
let mut buf = [0; 4];
match read_all(pipe, &mut buf) {
Ok(()) => {
let pid = i32::from_ne_bytes(buf);
trace!("spawned PID: {pid}");
// Start a systemd scope for the grandchild.
#[cfg(feature = "systemd")]
if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) {
trace!("error starting systemd scope for spawned command: {err:?}");
}
}
Err(err) => {
warn!("error reading child PID: {err:?}");
}
}
}
// Signal the intermediate child to exit now that we're done trying to creating a systemd
// scope.
trace!("signaling child to exit");
drop(pipe_wait_write);
Some(child)
}
let _span = tracy_client::span!();
#[cfg(feature = "systemd")]
fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
let mut written = 0;
loop {
let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
// Extract the basename.
let name = Path::new(name).file_name().unwrap_or(name);
let mut scope_name = String::from("app-niri-");
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
// systemd source.
for &c in name.as_bytes() {
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
scope_name.push(char::from(c));
} else {
let _ = write!(scope_name, "\\x{c:02x}");
written += n;
if written == buf.len() {
return Ok(());
}
}
}
let _ = write!(scope_name, "-{child_pid}.scope");
#[cfg(feature = "systemd")]
fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
let mut start = 0;
loop {
let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
if n == 0 {
return Err(rustix::io::Errno::CANCELED);
}
// Ask systemd to start a transient scope.
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
let conn = CONNECTION
.get_or_init(zbus::blocking::Connection::session)
.clone()
.context("error connecting to session bus")?;
let proxy = zbus::blocking::Proxy::new(
&conn,
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
)
.context("error creating a Proxy")?;
let signals = proxy
.receive_signal("JobRemoved")
.context("error creating a signal iterator")?;
let pids: &[_] = &[intermediate_pid, child_pid];
let properties: &[_] = &[
("PIDs", Value::new(pids)),
("CollectMode", Value::new("inactive-or-failed")),
];
let aux: &[(&str, &[(&str, Value)])] = &[];
let job: OwnedObjectPath = proxy
.call("StartTransientUnit", &(scope_name, "fail", properties, aux))
.context("error calling StartTransientUnit")?;
trace!("waiting for JobRemoved");
for message in signals {
let body: (u32, OwnedObjectPath, &str, &str) =
message.body().context("error parsing signal")?;
if body.1 == job {
// Our transient unit had started, we're good to exit the intermediate child.
break;
start += n;
if start == buf.len() {
return Ok(());
}
}
}
Ok(())
/// Puts a (newly spawned) pid into a transient systemd scope.
///
/// This separates the pid from the compositor scope, which for example prevents the OOM killer
/// from bringing down the compositor together with a misbehaving client.
#[cfg(feature = "systemd")]
fn start_systemd_scope(
name: &OsStr,
intermediate_pid: u32,
child_pid: u32,
) -> anyhow::Result<()> {
use std::fmt::Write as _;
use std::os::unix::ffi::OsStrExt;
use std::sync::OnceLock;
use anyhow::Context;
use zbus::zvariant::{OwnedObjectPath, Value};
use crate::utils::IS_SYSTEMD_SERVICE;
// We only start transient scopes if we're a systemd service ourselves.
if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) {
return Ok(());
}
let _span = tracy_client::span!();
// Extract the basename.
let name = Path::new(name).file_name().unwrap_or(name);
let mut scope_name = String::from("app-niri-");
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
// systemd source.
for &c in name.as_bytes() {
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
scope_name.push(char::from(c));
} else {
let _ = write!(scope_name, "\\x{c:02x}");
}
}
let _ = write!(scope_name, "-{child_pid}.scope");
// Ask systemd to start a transient scope.
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
let conn = CONNECTION
.get_or_init(zbus::blocking::Connection::session)
.clone()
.context("error connecting to session bus")?;
let proxy = zbus::blocking::Proxy::new(
&conn,
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
)
.context("error creating a Proxy")?;
let signals = proxy
.receive_signal("JobRemoved")
.context("error creating a signal iterator")?;
let pids: &[_] = &[intermediate_pid, child_pid];
let properties: &[_] = &[
("PIDs", Value::new(pids)),
("CollectMode", Value::new("inactive-or-failed")),
];
let aux: &[(&str, &[(&str, Value)])] = &[];
let job: OwnedObjectPath = proxy
.call("StartTransientUnit", &(scope_name, "fail", properties, aux))
.context("error calling StartTransientUnit")?;
trace!("waiting for JobRemoved");
for message in signals {
let body: (u32, OwnedObjectPath, &str, &str) =
message.body().context("error parsing signal")?;
if body.1 == job {
// Our transient unit had started, we're good to exit the intermediate child.
break;
}
}
Ok(())
}
}
+275
View File
@@ -0,0 +1,275 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use niri_config::{BlockOutFrom, WindowRule};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{AsRenderElements as _, Id, Kind};
use smithay::desktop::space::SpaceElement as _;
use smithay::desktop::Window;
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_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
use super::{ResolvedWindowRules, WindowRef};
use crate::layout::{LayoutElement, LayoutElementRenderElement};
use crate::niri::WindowOffscreenId;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
#[derive(Debug)]
pub struct Mapped {
pub window: Window,
/// Up-to-date rules.
rules: ResolvedWindowRules,
/// Whether the window rules need to be recomputed.
///
/// This is not used in all cases; for example, app ID and title changes recompute the rules
/// immediately, rather than setting this flag.
need_to_recompute_rules: bool,
/// Whether this window has the keyboard focus.
is_focused: bool,
/// Buffer to draw instead of the window when it should be blocked out.
block_out_buffer: RefCell<SolidColorBuffer>,
}
impl Mapped {
pub fn new(window: Window, rules: ResolvedWindowRules) -> Self {
Self {
window,
rules,
need_to_recompute_rules: false,
is_focused: false,
block_out_buffer: RefCell::new(SolidColorBuffer::new((0, 0), [0., 0., 0., 1.])),
}
}
pub fn toplevel(&self) -> &ToplevelSurface {
self.window.toplevel().expect("no X11 support")
}
/// Recomputes the resolved window rules and returns whether they changed.
pub fn recompute_window_rules(&mut self, rules: &[WindowRule]) -> bool {
self.need_to_recompute_rules = false;
let new_rules = ResolvedWindowRules::compute(rules, WindowRef::Mapped(self));
if new_rules == self.rules {
return false;
}
self.rules = new_rules;
true
}
pub fn recompute_window_rules_if_needed(&mut self, rules: &[WindowRule]) -> bool {
if !self.need_to_recompute_rules {
return false;
}
self.recompute_window_rules(rules)
}
pub fn is_focused(&self) -> bool {
self.is_focused
}
pub fn set_is_focused(&mut self, is_focused: bool) {
if self.is_focused == is_focused {
return;
}
self.is_focused = is_focused;
self.need_to_recompute_rules = true;
}
}
impl LayoutElement for Mapped {
type Id = Window;
fn id(&self) -> &Self::Id {
&self.window
}
fn size(&self) -> Size<i32, Logical> {
self.window.geometry().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
Point::from((0, 0)) - self.window.geometry().loc
}
fn is_in_input_region(&self, point: Point<f64, Logical>) -> bool {
let surface_local = point + self.window.geometry().loc.to_f64();
self.window.is_in_input_region(&surface_local)
}
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
let block_out = match self.rules.block_out_from {
None => false,
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
};
if block_out {
let mut buffer = self.block_out_buffer.borrow_mut();
buffer.resize(self.window.geometry().size);
let elem = SolidColorRenderElement::from_buffer(
&buffer,
location.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
);
vec![elem.into()]
} else {
let buf_pos = location - self.window.geometry().loc;
self.window.render_elements(
renderer,
buf_pos.to_physical_precise_round(scale),
scale,
alpha,
)
}
}
fn request_size(&self, size: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.size = Some(size);
state.states.unset(xdg_toplevel::State::Fullscreen);
});
}
fn request_fullscreen(&self, size: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.size = Some(size);
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
fn min_size(&self) -> Size<i32, Logical> {
let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.min_size
});
if let Some(x) = self.rules.min_width {
size.w = max(size.w, i32::from(x));
}
if let Some(x) = self.rules.min_height {
size.h = max(size.h, i32::from(x));
}
size
}
fn max_size(&self) -> Size<i32, Logical> {
let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.max_size
});
if let Some(x) = self.rules.max_width {
if size.w == 0 {
size.w = i32::from(x);
} else if x > 0 {
size.w = min(size.w, i32::from(x));
}
}
if let Some(x) = self.rules.max_height {
if size.h == 0 {
size.h = i32::from(x);
} else if x > 0 {
size.h = min(size.h, i32::from(x));
}
}
size
}
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool {
self.toplevel().wl_surface() == wl_surface
}
fn set_preferred_scale_transform(&self, scale: i32, transform: Transform) {
self.window.with_surfaces(|surface, data| {
send_surface_state(surface, data, scale, transform);
});
}
fn has_ssd(&self) -> bool {
self.toplevel().current_state().decoration_mode
== Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
}
fn output_enter(&self, output: &Output) {
let overlap = Rectangle::from_loc_and_size((0, 0), (i32::MAX, i32::MAX));
self.window.output_enter(output, overlap)
}
fn output_leave(&self, output: &Output) {
self.window.output_leave(output)
}
fn set_offscreen_element_id(&self, id: Option<Id>) {
let data = self
.window
.user_data()
.get_or_insert(WindowOffscreenId::default);
data.0.replace(id);
}
fn set_activated(&mut self, active: bool) {
let changed = self.toplevel().with_pending_state(|state| {
if active {
state.states.set(xdg_toplevel::State::Activated)
} else {
state.states.unset(xdg_toplevel::State::Activated)
}
});
self.need_to_recompute_rules |= changed;
}
fn set_bounds(&self, bounds: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.bounds = Some(bounds);
});
}
fn send_pending_configure(&self) {
self.toplevel().send_pending_configure();
}
fn is_fullscreen(&self) -> bool {
self.toplevel()
.current_state()
.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))
}
fn refresh(&self) {
self.window.refresh();
}
fn rules(&self) -> &ResolvedWindowRules {
&self.rules
}
}
+218
View File
@@ -0,0 +1,218 @@
use niri_config::{BlockOutFrom, Match, WindowRule};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use crate::layout::workspace::ColumnWidth;
pub mod mapped;
pub use mapped::Mapped;
pub mod unmapped;
pub use unmapped::{InitialConfigureState, Unmapped};
/// Reference to a mapped or unmapped window.
#[derive(Debug, Clone, Copy)]
pub enum WindowRef<'a> {
Unmapped(&'a Unmapped),
Mapped(&'a Mapped),
}
/// Rules fully resolved for a window.
#[derive(Debug, PartialEq)]
pub struct ResolvedWindowRules {
/// Default width for this window.
///
/// - `None`: unset (global default should be used).
/// - `Some(None)`: set to empty (window picks its own width).
/// - `Some(Some(width))`: set to a particular width.
pub default_width: Option<Option<ColumnWidth>>,
/// Output to open this window on.
pub open_on_output: Option<String>,
/// Whether the window should open full-width.
pub open_maximized: Option<bool>,
/// Whether the window should open fullscreen.
pub open_fullscreen: Option<bool>,
/// Extra bound on the minimum window width.
pub min_width: Option<u16>,
/// Extra bound on the minimum window height.
pub min_height: Option<u16>,
/// Extra bound on the maximum window width.
pub max_width: Option<u16>,
/// Extra bound on the maximum window height.
pub max_height: Option<u16>,
/// Whether or not to draw the border with a solid background.
///
/// `None` means using the SSD heuristic.
pub draw_border_with_background: Option<bool>,
/// Extra opacity to draw this window with.
pub opacity: Option<f32>,
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
}
impl<'a> WindowRef<'a> {
pub fn toplevel(self) -> &'a ToplevelSurface {
match self {
WindowRef::Unmapped(unmapped) => unmapped.toplevel(),
WindowRef::Mapped(mapped) => mapped.toplevel(),
}
}
pub fn is_focused(self) -> bool {
match self {
WindowRef::Unmapped(_) => false,
WindowRef::Mapped(mapped) => mapped.is_focused(),
}
}
}
impl ResolvedWindowRules {
pub const fn empty() -> Self {
Self {
default_width: None,
open_on_output: None,
open_maximized: None,
open_fullscreen: None,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
draw_border_with_background: None,
opacity: None,
block_out_from: None,
}
}
pub fn compute(rules: &[WindowRule], window: WindowRef) -> Self {
let _span = tracy_client::span!("ResolvedWindowRules::compute");
let mut resolved = ResolvedWindowRules::empty();
let toplevel = window.toplevel();
with_states(toplevel.wl_surface(), |states| {
let mut role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
// Ensure server_pending like in Smithay's with_pending_state().
if role.server_pending.is_none() {
role.server_pending = Some(role.current_server_state().clone());
}
let mut open_on_output = None;
for rule in rules {
let matches = |m| window_matches(window, &role, m);
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
continue;
}
if rule.excludes.iter().any(matches) {
continue;
}
if let Some(x) = rule
.default_column_width
.as_ref()
.map(|d| d.0.map(ColumnWidth::from))
{
resolved.default_width = Some(x);
}
if let Some(x) = rule.open_on_output.as_deref() {
open_on_output = Some(x);
}
if let Some(x) = rule.open_maximized {
resolved.open_maximized = Some(x);
}
if let Some(x) = rule.open_fullscreen {
resolved.open_fullscreen = Some(x);
}
if let Some(x) = rule.min_width {
resolved.min_width = Some(x);
}
if let Some(x) = rule.min_height {
resolved.min_height = Some(x);
}
if let Some(x) = rule.max_width {
resolved.max_width = Some(x);
}
if let Some(x) = rule.max_height {
resolved.max_height = Some(x);
}
if let Some(x) = rule.draw_border_with_background {
resolved.draw_border_with_background = Some(x);
}
if let Some(x) = rule.opacity {
resolved.opacity = Some(x);
}
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
});
resolved
}
}
fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
// Must be ensured by the caller.
let server_pending = role.server_pending.as_ref().unwrap();
if let Some(is_focused) = m.is_focused {
if window.is_focused() != is_focused {
return false;
}
}
if let Some(is_active) = m.is_active {
// Our "is-active" definition corresponds to the window having a pending Activated state.
let pending_activated = server_pending
.states
.contains(xdg_toplevel::State::Activated);
if is_active != pending_activated {
return false;
}
}
if let Some(app_id_re) = &m.app_id {
let Some(app_id) = &role.app_id else {
return false;
};
if !app_id_re.is_match(app_id) {
return false;
}
}
if let Some(title_re) = &m.title {
let Some(title) = &role.title else {
return false;
};
if !title_re.is_match(title) {
return false;
}
}
true
}
+6 -20
View File
@@ -1,6 +1,8 @@
use smithay::desktop::Window;
use smithay::output::Output;
use smithay::wayland::shell::xdg::ToplevelSurface;
use super::ResolvedWindowRules;
use crate::layout::workspace::ColumnWidth;
#[derive(Debug)]
@@ -43,26 +45,6 @@ pub enum InitialConfigureState {
},
}
/// Rules fully resolved for a window.
#[derive(Debug, Default)]
pub struct ResolvedWindowRules {
/// Default width for this window.
///
/// - `None`: unset (global default should be used).
/// - `Some(None)`: set to empty (window picks its own width).
/// - `Some(Some(width))`: set to a particular width.
pub default_width: Option<Option<ColumnWidth>>,
/// Output to open this window on.
pub open_on_output: Option<String>,
/// Whether the window should open full-width.
pub open_maximized: Option<bool>,
/// Whether the window should open fullscreen.
pub open_fullscreen: Option<bool>,
}
impl Unmapped {
/// Wraps a newly created window that hasn't been initially configured yet.
pub fn new(window: Window) -> Self {
@@ -77,4 +59,8 @@ impl Unmapped {
pub fn needs_initial_configure(&self) -> bool {
matches!(self.state, InitialConfigureState::NotConfigured { .. })
}
pub fn toplevel(&self) -> &ToplevelSurface {
self.window.toplevel().expect("no X11 support")
}
}
+30
View File
@@ -0,0 +1,30 @@
### VSCode
There seems to be a bug in VSCode's Wayland backend until 1.86.0 which causes the window to not show up when using server-side decorations. So, to run VSCode:
1. Make sure VSCode is 1.86.0 or above, or that `prefer-no-csd` is **not set** in the niri config
2. Run `code --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations`
Also, if you're having issues with some VSCode hotkeys, try starting `Xwayland` and setting the `DISPLAY=:0` environment variable for VSCode. That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance. Apparently, VSCode currently unconditionally queries the X server for a keymap.
### Chromium
When creating new windows within Chromium (e.g. with <kbd>Ctrl</kbd><kbd>N</kbd>), there's a Chromium bug with sizing:
- With CSD (`prefer-no-csd` unset), the window will be a bit smaller than needed
- With SSD (`prefer-no-csd` set), the window buffer will be offset to the top-left
Both of these can be fixed by resizing the new Chromium window.
### WezTerm
There's [a bug](https://github.com/wez/wezterm/issues/4708) in WezTerm that it waits for a zero-sized Wayland configure event, so its window never shows up in niri. To work around it, put this window rule in the niri config (included in the default config):
```
window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
```
This empty default column width lets WezTerm pick its own initial width which makes it show up properly.
+161
View File
@@ -0,0 +1,161 @@
### Overview
Niri has several animations which you can configure in the same way.
Additionally, you can disable or slow down all animations at once.
Here's a quick glance at the available animations with their default values.
```
animations {
// Uncomment to turn off all animations.
// You can also put "off" into each individual animation to disable it.
// off
// Slow down all animations by this factor. Values below 1 speed them up instead.
// slowdown 3.0
// Individual animations.
workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
horizontal-view-movement {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
window-open {
duration-ms 150
curve "ease-out-expo"
}
config-notification-open-close {
spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
}
}
```
### Animation Types
There are two animation types: easing and spring.
Each animation can be either an easing or a spring.
#### Easing
This is a relatively common animation type that changes the value over a set duration using an interpolation curve.
To use this animation, set the following parameters:
- `duration-ms`: duration of the animation in milliseconds.
- `curve`: the easing curve to use.
```
animations {
window-open {
duration-ms 150
curve "ease-out-expo"
}
}
```
Currently, niri only supports two curves: `ease-out-cubic` and `ease-out-expo`.
You can get a feel for them on pages like [easings.net](https://easings.net/).
#### Spring
Spring animations use a model of a physical spring to animate the value.
They notably feel better with touchpad gestures, because they take into account the velocity of your fingers as you release the swipe.
Springs can also oscillate / bounce at the end with the right parameters if you like that sort of thing, but they don't have to (and by default they mostly don't).
Due to springs using a physical model, the animation parameters are less obvious and generally should be tuned with trial and error.
Notably, you cannot directly set the duration.
You can use the [Elastic](https://flathub.org/apps/app.drey.Elastic) app to help visualize how the spring parameters change the animation.
A spring animation is configured like this, with three mandatory parameters:
```
animations {
workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
}
```
The `damping-ratio` goes from 0.1 to 10.0 and has the following properties:
- below 1.0: underdamped spring, will oscillate in the end.
- above 1.0: overdamped spring, won't oscillate.
- 1.0: critically damped spring, comes to rest in minimum possible time without oscillations.
However, even with damping ratio = 1.0, the spring animation may oscillate if "launched" with enough velocity from a touchpad swipe.
Lower `stiffness` will result in a slower animation more prone to oscillation.
Set `epsilon` to a lower value if the animation "jumps" at the end.
> [!TIP]
> The spring *mass* (which you can see in Elastic) is hardcoded to 1.0 and cannot be changed.
> Instead, change `stiffness` proportionally.
> E.g. increasing mass by 2× is the same as decreasing stiffness by 2×.
### Animations
Now let's go into more detail on the animations that you can configure.
#### `workspace-switch`
Animation when switching workspaces up and down, including after the vertical touchpad gesture (a spring is recommended).
```
animations {
workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
}
```
#### `horizontal-view-movement`
All horizontal camera view movement animations, such as:
- When a window off-screen is focused and the camera scrolls to it.
- When a new window appears off-screen and the camera scrolls to it.
- When a window resizes bigger and the camera scrolls to show it in full.
- After a horizontal touchpad gesture (a spring is recommended).
```
animations {
horizontal-view-movement {
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
}
}
```
#### `window-open`
Window opening animation.
This one uses an easing type by default.
```
animations {
window-open {
duration-ms 150
curve "ease-out-expo"
}
}
```
#### `config-notification-open-close`
The open/close animation of the config parse error and new default config notifications.
This one uses an underdamped spring by default (`damping-ratio=0.6`) which causes a slight oscillation in the end.
```
animations {
config-notification-open-close {
spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
}
}
```
+140
View File
@@ -0,0 +1,140 @@
### Overview
Niri has several options that are only useful for debugging, or are experimental and have known issues.
They are not meant for normal use.
> [!CAUTION]
> These options are **not** covered by the [config breaking change policy](./Configuration:-Overview.md).
> They can change or stop working at any point with little notice.
Here are all the options at a glance:
```
debug {
preview-render "screencast"
// preview-render "screen-capture"
enable-overlay-planes
disable-cursor-plane
render-drm-device "/dev/dri/renderD129"
dbus-interfaces-in-non-session-instances
wait-for-frame-completion-before-queueing
emulate-zero-presentation-time
enable-color-transformations-capability
}
```
### `preview-render`
Make niri render the monitors the same way as for a screencast or a screen capture.
Useful for previewing the `block-out-from` window rule.
```
debug {
preview-render "screencast"
// preview-render "screen-capture"
}
```
### `enable-overlay-planes`
Enable direct scanout into overlay planes.
May cause frame drops during some animations on some hardware (which is why it is not the default).
Direct scanout into the primary plane is always enabled.
```
debug {
enable-overlay-planes
}
```
### `disable-cursor-plane`
Disable the use of the cursor plane.
The cursor will be rendered together with the rest of the frame.
Useful to work around driver bugs on specific hardware.
```
debug {
disable-cursor-plane
}
```
### `render-drm-device`
Override the DRM device that niri will use for all rendering.
You can set this to make niri use a different primary GPU than the default one.
```
debug {
render-drm-device "/dev/dri/renderD129"
}
```
### `dbus-interfaces-in-non-session-instances`
Make niri create its D-Bus interfaces even if it's not running as a `--session`.
Useful for testing screencasting changes without having to relogin.
The main niri instance will *not* currently take back the interfaces when you close the test instance, so you will need to relogin in the end to make screencasting work again.
```
debug {
dbus-interfaces-in-non-session-instances
}
```
### `wait-for-frame-completion-before-queueing`
Wait until every frame is done rendering before handing it over to DRM.
Useful for diagnosing certain synchronization and performance problems.
```
debug {
wait-for-frame-completion-before-queueing
}
```
### `emulate-zero-presentation-time`
Emulate zero (unknown) presentation time returned from DRM.
This is a thing on NVIDIA proprietary drivers, so this flag can be used to test that niri doesn't break too hard on those systems.
```
debug {
emulate-zero-presentation-time
}
```
### `enable-color-transformations-capability`
Enable the color-transformations capability of the Smithay renderer.
May cause a slight decrease in rendering performance.
Currently, should cause no visible changes in behavior, but it will be needed for HDR support whenever that happens.
So, this flag exists to be able to make sure that nothing breaks.
```
debug {
enable-color-transformations-capability
}
```
### `toggle-debug-tint` Key Binding
This one is not a debug option, but rather a key binding.
It will tint all surfaces green, unless they are being directly scanned out.
It's therefore useful to check if direct scanout is working.
```
binds {
Mod+Shift+Ctrl+T { toggle-debug-tint; }
}
```
+197
View File
@@ -0,0 +1,197 @@
### Overview
In this section you can configure input devices like keyboard and mouse, and some input-related options.
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `tablet`, `touch`.
Settings in those sections will apply to every device of that type.
Currently, there's no way to configure specific devices individually (but that is planned).
All settings at a glance:
```
input {
keyboard {
xkb {
// layout "us"
// variant "colemak_dh_ortho"
// options "compose:ralt,ctrl:nocaps"
// model ""
// rules ""
}
// repeat-delay 600
// repeat-rate 25
// track-layout "global"
}
touchpad {
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// tap-button-map "left-middle-right"
// click-method "clickfinger"
}
mouse {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
trackpoint {
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
tablet {
map-to-output "eDP-1"
}
touch {
map-to-output "eDP-1"
}
// disable-power-key-handling
// warp-mouse-to-focus
// focus-follows-mouse
// workspace-auto-back-and-forth
}
```
### Keyboard
#### Layout
In the `xkb` section, you can set layout, variant, options, model and rules.
These are passed directly to libxkbcommon, which is also used by most other Wayland compositors.
See the `xkeyboard-config(7)` manual for more information.
```
input {
keyboard {
xkb {
layout "us"
variant "colemak_dh_ortho"
options "compose:ralt,ctrl:nocaps"
}
}
}
```
When using multiple layouts, niri can remember the current layout globally (the default) or per-window.
You can control this with the `track-layout` option.
- `global`: layout change is global for all windows.
- `window`: layout is tracked for each window individually.
```
input {
keyboard {
track-layout "global"
}
}
```
#### Repeat
Delay is in milliseconds before the keyboard repeat starts.
Rate is in characters per second.
```
input {
keyboard {
repeat-delay 600
repeat-rate 25
}
}
```
### Pointing Devices
Most settings for the pointing devices are passed directly to libinput.
Other Wayland compositors also use libinput, so it's likely you will find the same settings there.
For flags like `tap`, omit them or comment them out to disable the setting.
A few settings are common between `touchpad`, `mouse` and `trackpoint`:
- `natural-scroll`: if set, inverts the scrolling direction.
- `accel-speed`: pointer acceleration speed, valid values are from `-1.0` to `1.0` where the default is `0.0`.
- `accel-profile`: can be `adaptive` (the default) or `flat` (disables pointer acceleration).
Settings specific to `touchpad`s:
- `tap`: tap-to-click.
- `dwt`: disable-when-typing.
- `dwtp`: disable-when-trackpointing.
- `tap-button-map`: can be `left-right-middle` or `left-middle-right`, controls which button corresponds to a two-finger tap and a three-finger tap.
- `click-method`: can be `button-areas` or `clickfinger`, changes the [click method](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html).
Tablets and touchscreens are absolute pointing devices that can be mapped to a specific output like so:
```
input {
tablet {
map-to-output "eDP-1"
}
touch {
map-to-output "eDP-1"
}
}
```
Valid output names are the same as the ones used for output configuration.
### General Settings
These settings are not specific to a particular input device.
#### `disable-power-key-handling`
By default, niri will take over the power button to make it sleep instead of power off.
Set this if you would like to configure the power button elsewhere (i.e. `logind.conf`).
```
input {
disable-power-key-handling
}
```
#### `warp-mouse-to-focus`
Makes the mouse warp to newly focused windows.
X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
```
input {
warp-mouse-to-focus
}
```
#### `focus-follows-mouse`
Focuses windows and outputs automatically when moving the mouse over them.
```
input {
focus-follows-mouse
}
```
#### `workspace-auto-back-and-forth`
Normally, switching to the same workspace by index twice will do nothing (since you're already on that workspace).
If this flag is enabled, switching to the same workspace by index twice will switch back to the previous workspace.
Niri will correctly switch to the workspace you came from, even if workspaces were reordered in the meantime.
```
input {
workspace-auto-back-and-forth
}
```
+175
View File
@@ -0,0 +1,175 @@
### Overview
Key bindings are declared in the `binds {}` section of the config.
> [!NOTE]
> This is one of the few sections that *does not* get automatically filled with defaults if you omit it, so make sure to copy it from the default config.
Each bind is a hotkey followed by one action enclosed in curly brackets.
For example:
```
binds {
Mod+Left { focus-column-left; }
Super+Alt+L { spawn "swaylock"; }
}
```
The hotkey consists of modifiers separated by `+` signs, followed by an XKB key name in the end.
Valid modifiers are:
- `Ctrl` or `Control`;
- `Shift`;
- `Alt`;
- `Super` or `Win`;
- `ISO_Level3_Shift` or `Mod5`—this is the AltGr key on certain layouts;
- `Mod`.
`Mod` is a special modifier that is equal to `Super` when running niri on a TTY, and to `Alt` when running niri as a nested winit window.
This way, you can test niri in a window without causing too many conflicts with the host compositor's key bindings.
For this reason, most of the default keys use the `Mod` modifier.
> [!TIP]
> To find an XKB name for a particular key, you may use a program like [`wev`](https://git.sr.ht/~sircmpwn/wev).
>
> Open it from a terminal and press the key that you want to detect.
> In the terminal, you will see output like this:
>
> ```
> [14: wl_keyboard] key: serial: 757775; time: 44940343; key: 113; state: 1 (pressed)
> sym: Left (65361), utf8: ''
> [14: wl_keyboard] key: serial: 757776; time: 44940432; key: 113; state: 0 (released)
> sym: Left (65361), utf8: ''
> [14: wl_keyboard] key: serial: 757777; time: 44940753; key: 114; state: 1 (pressed)
> sym: Right (65363), utf8: ''
> [14: wl_keyboard] key: serial: 757778; time: 44940846; key: 114; state: 0 (released)
> sym: Right (65363), utf8: ''
> ```
>
> Here, look at `sym: Left` and `sym: Right`: these are the key names.
> I was pressing the left and the right arrow in this example.
Binds can also have a cooldown, which will rate-limit the bind and prevent it from repeatedly triggering too quickly.
```
binds {
Mod+T cooldown-ms=500 { spawn "alacritty"; }
}
```
This is mostly useful for the scroll bindings.
### Scroll Bindings
You can bind mouse wheel scroll ticks using the following syntax.
These binds will change direction based on the `natural-scroll` setting.
```
binds {
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
}
```
Similarly, you can bind touchpad scroll "ticks".
Touchpad scrolling is continuous, so for these binds it is split into discrete intervals based on distance travelled.
These binds are also affected by touchpad's `natural-scroll`, so these example binds are "inverted", since niri has `natural-scroll` enabled for touchpads by default.
```
binds {
Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; }
Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; }
}
```
Both mouse wheel and touchpad scroll binds will prevent applications from receiving any scroll events when their modifiers are held down.
For example, if you have a `Mod+WheelScrollDown` bind, then while holding `Mod`, all mouse wheel scrolling will be consumed by niri.
### Actions
Every action that you can bind is also available for programmatic invocation via `niri msg action`.
Run `niri msg action` to get a full list of actions along with their short descriptions.
Here are a few actions that benefit from more explanation.
#### `spawn`
Run a program.
`spawn` accepts a path to the program binary as the first argument, followed by arguments to the program.
For example:
```
binds {
// Run alacritty.
Mod+T { spawn "alacritty"; }
// Run `wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1+`.
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
}
```
Currently, niri *does not* use a shell to run commands, which means that you need to manually separate arguments.
```
binds {
// Correct: every argument is in its own quotes.
Mod+T { spawn "alacritty" "-e" "/usr/bin/fish"; }
// Wrong: will interpret the whole `alacritty -e /usr/bin/fish` string as the binary path.
Mod+T { spawn "alacritty -e /usr/bin/fish"; }
// Wrong: will pass `-e /usr/bin/fish` as one argument, which alacritty won't understand.
Mod+T { spawn "alacritty" "-e /usr/bin/fish"; }
}
```
This also means that you cannot expand environment variables or `~`.
If you need this, you can run the command through a shell manually.
```
binds {
// Wrong: no shell expansion here. These strings will be passed literally to the program.
Mod+T { spawn "grim" "-o" "$MAIN_OUTPUT" "~/screenshot.png"; }
// Correct: run this through a shell manually so that it can expand the arguments.
// Note that the entire command is passed as a SINGLE argument,
// because shell will do its own argument splitting by whitespace.
Mod+T { spawn "sh" "-c" "grim -o $MAIN_OUTPUT ~/screenshot.png"; }
// You can also use a shell to run multiple commands,
// use pipes, process substitution, and so on.
Mod+T { spawn "sh" "-c" "notify-send clipboard \"$(wl-paste)\""; }
}
```
As a special case, niri will expand `~` to the home directory *only* at the beginning of the program name.
```
binds {
// This will work: one ~ at the very beginning.
Mod+T { spawn "~/scripts/do-something.sh"; }
}
```
#### `quit`
Exit niri after showing a confirmation dialog to avoid accidentally triggering it.
```
binds {
Mod+Shift+E { quit; }
}
```
If you want to skip the confirmation dialog, set the flag like so:
```
binds {
Mod+Shift+E { quit skip-confirmation=true; }
}
```
+241
View File
@@ -0,0 +1,241 @@
### Overview
In the `layout {}` section you can change various settings that influence how windows are positioned and sized.
Here are the contents of this section at a glance:
```
layout {
gaps 16
center-focused-column "never"
preset-column-widths {
proportion 0.33333
proportion 0.5
proportion 0.66667
}
default-column-width { proportion 0.5; }
focus-ring {
// off
width 4
active-color "#7fc8ff"
inactive-color "#505050"
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
border {
off
width 4
active-color "#ffc87f"
inactive-color "#505050"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
struts {
// left 64
// right 64
// top 64
// bottom 64
}
}
```
### `gaps`
Set gaps around (inside and outside) windows in logical pixels.
```
layout {
gaps 16
}
```
### `center-focused-column`
When to center a column when changing focus.
This can be set to:
- `"never"`: no special centering, focusing an off-screen column will scroll it to the left or right edge of the screen. This is the default.
- `"always"`, the focused column will always be centered.
- `"on-overflow"`, focusing a column will center it if it doesn't fit on screen together with the previously focused column.
```
layout {
center-focused-column "always"
}
```
### `preset-column-widths`
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
`proportion` sets the width as a fraction of the output width, taking gaps into account.
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
The default preset widths are <sup>1</sup>&frasl;<sub>3</sub>, <sup>1</sup>&frasl;<sub>2</sub> and <sup>2</sup>&frasl;<sub>3</sub> of the output.
`fixed` sets the width in logical pixels exactly.
```
layout {
// Cycle between 1/3, 1/2, 2/3 of the output, and a fixed 1280 logical pixels.
preset-column-widths {
proportion 0.33333
proportion 0.5
proportion 0.66667
fixed 1280
}
}
```
> [!NOTE]
> Currently, due to an oversight, a preset `fixed` width does not take borders into account.
> I.e., preset `fixed 1000` with 4-wide borders will make the window 992 logical pixels wide.
> This may eventually be corrected.
>
> All other ways of using `fixed` (i.e. `default-column-width` or `set-column-width`) do take borders into account and give you the exact window width that you request.
### `default-column-width`
Set the default width of the new windows.
The syntax is the same as in `preset-column-widths` above.
```
layout {
// Open new windows sized 1/3 of the output.
default-column-width { proportion 0.33333; }
}
```
You can also leave the brackets empty, then the windows themselves will decide their initial width.
```
layout {
// New windows decide their initial width themselves.
default-column-width {}
}
```
> [!NOTE]
> `default-column-width {}` causes niri to send a (0, H) size in the initial configure request.
>
> This is a bit [unclearly defined](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/155) in the Wayland protocol, so some clients may misinterpret it.
> In practice, the only problematic client I saw is [foot](https://codeberg.org/dnkl/foot/), which takes this as a request to have a literal zero width.
>
> Either way, `default-column-width {}` is most useful for specific windows, in form of a [window rule](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules) with the same syntax.
### `focus-ring` and `border`
Focus ring and border are drawn around windows and indicate the active window.
They are very similar and have the same options.
The difference is that the focus ring is drawn only around the active window, whereas borders are drawn around all windows and affect their sizes (windows shrink to make space for the borders).
| Focus Ring | Border |
| ---------- | ------ |
| ![](./img/focus-ring.png) | ![](./img/border.png) |
> [!TIP]
> 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 decorations can have an arbitrary shape.
>
> If you don't like that, you should uncomment the `prefer-no-csd` setting at the [top level](./Configuration:-Miscellaneous.md) of the config.
> Niri will draw focus rings and borders *around* windows that agree to omit their client-side decorations.
>
> Alternatively, you can override this behavior with the `draw-border-with-background` [window rule](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules).
Focus ring and border have the following options.
```
layout {
// focus-ring has the same options.
border {
// Uncomment this line to disable the border.
// off
// Width of the border in logical pixels.
width 4
active-color "#ffc87f"
inactive-color "#505050"
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
}
```
#### Colors
Colors can be set in a variety of ways:
- CSS named colors: `"red"`
- RGB hex: `"#rgb"`, `"#rgba"`, `"#rrggbb"`, `"#rrggbbaa"`
- CSS-like notation: `"rgb(255, 127, 0)"`, `"rgba()"`, `"hsl()"` and a few others.
`active-color` is the color of the focus ring / border around the active window, and `inactive-color` is the color of the focus ring / border around all other windows.
The *focus ring* is only drawn around the active window on each monitor, so with a single monitor you will never see its `inactive-color`.
You will see it if you have multiple monitors, though.
There's also a *deprecated* syntax for setting colors with four numbers representing R, G, B and A: `active-color 127 200 255 255`.
#### Gradients
Similarly to colors, you can set `active-gradient` and `inactive-gradient`, which will take precedence.
Gradients are rendered the same as CSS [`linear-gradient(angle, from, to)`](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient).
The angle works the same as in `linear-gradient`, and is optional, defaulting to `180` (top-to-bottom gradient).
You can use any CSS linear-gradient tool on the web to set these up, like [this one](https://www.css-gradient.com/).
```
layout {
focus-ring {
active-gradient from="#80c8ff" to="#bbddff" angle=45
}
}
```
Gradients can be colored relative to windows individually (the default), or to the whole view of the workspace.
To do that, set `relative-to="workspace-view"`.
Here's a visual example:
| Default | `relative-to="workspace-view"` |
| --- | --- |
| ![](./img/gradients-default.png) | ![](./img/gradients-relative-to-workspace-view.png) |
```
layout {
border {
active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
}
```
### `struts`
Struts shrink the area occupied by windows, similarly to layer-shell panels.
You can think of them as a kind of outer gaps.
They are set in logical pixels.
Left and right struts will cause the next window to the side to always peek out slightly.
Top and bottom struts will simply add outer gaps in addition to the area occupied by layer-shell panels and regular gaps.
```
layout {
struts {
left 64
right 64
top 64
bottom 64
}
}
```
![](./img/struts.png)
+119
View File
@@ -0,0 +1,119 @@
### Overview
This page documents all top-level options that don't otherwise have dedicated pages.
Here are all of these options at a glance:
```
spawn-at-startup "waybar"
spawn-at-startup "alacritty"
prefer-no-csd
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
environment {
QT_QPA_PLATFORM "wayland"
DISPLAY null
}
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 48
}
hotkey-overlay {
skip-at-startup
}
```
### `spawn-at-startup`
Add lines like this to spawn processes at niri startup.
`spawn-at-startup` accepts a path to the program binary as the first argument, followed by arguments to the program.
This option works the same way as the `spawn` key binding action, so please read about all its subtleties on the [key bindings](./Configuration:-Key-Bindings.md) page.
```
spawn-at-startup "waybar"
spawn-at-startup "alacritty"
```
Note that running niri as a systemd session supports xdg-desktop-autostart out of the box, which may be more convenient to use.
Thanks to this, apps that you configured to autostart in GNOME will also "just work" in niri, without any manual `spawn-at-startup` configuration.
### `prefer-no-csd`
This flag will make niri ask the applications to omit their client-side decorations.
If an application will specifically ask for CSD, the request will be honored.
Additionally, clients will be informed that they are tiled, removing some rounded corners.
With `prefer-no-csd` set, applications that negotiate server-side decorations through the xdg-decoration protocol will have focus ring and border drawn around them *without* a solid colored background.
> [!NOTE]
> Unlike most other options, changing `prefer-no-csd` will not affect already running applications.
> This mainly has to do with niri working around a [bug in SDL2](https://github.com/libsdl-org/SDL/issues/8173) that prevents SDL2 applications from starting.
>
> Restart applications after changing `prefer-no-csd` in the config to apply it.
```
prefer-no-csd
```
### `screenshot-path`
Set the path where screenshots are saved.
A `~` at the front will be expanded to the home directory.
The path is formatted with `strftime(3)` to give you the screenshot date and time.
Niri will create the last folder of the path if it doesn't exist.
```
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
```
You can also set this option to `null` to disable saving screenshots to disk.
```
screenshot-path null
```
### `environment`
Override environment variables for processes spawned by niri.
```
environment {
// Set a variable like this:
// QT_QPA_PLATFORM "wayland"
// Remove a variable by using null as the value:
// DISPLAY null
}
```
### `cursor`
Change the theme and size of the cursor as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE` environment variables.
```
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 48
}
```
### `hotkey-overlay`
Settings for the "Important Hotkeys" overlay.
Set the `skip-at-startup` flag if you don't want to see the hotkey help at niri startup.
```
hotkey-overlay {
skip-at-startup
}
```
+115
View File
@@ -0,0 +1,115 @@
### Overview
By default, niri will attempt to turn on all connected monitors using their preferred modes.
You can disable or adjust this with `output` sections.
Here's what it looks like with all properties written out:
```
output "eDP-1" {
// off
mode "1920x1080@120.030"
scale 2.0
transform "90"
position x=1280 y=0
}
output "HDMI-A-1" {
// ...settings for HDMI-A-1...
}
```
Outputs are matched by connector name (i.e. `eDP-1`, `HDMI-A-1`) which you can find by running `niri msg outputs`.
Usually, the built-in monitor in laptops will be called `eDP-1`.
Matching by output manufacturer and model is planned, but blocked on Smithay adopting libdisplay-info instead of edid-rs.
### `off`
This flag turns off that output entirely.
```
// Turn off that monitor.
output "HDMI-A-1" {
off
}
```
### `mode`
Set the monitor resolution and refresh rate.
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 doesn't work, niri will try to pick one automatically.
Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
The refresh rate that you set here must match *exactly*, down to the three decimal digits, to what you see in `niri msg outputs`.
```
// Set a high refresh rate for this monitor.
// High refresh rate monitors tend to use 60 Hz as their preferred mode,
// requiring a manual mode setting.
output "HDMI-A-1" {
mode "2560x1440@143.912"
}
// Use a lower resolution on the built-in laptop monitor
// (for example, for testing purposes).
output "eDP-1" {
mode "1280x720"
}
```
### `scale`
Set the scale of the monitor.
This is a floating-point number to enable fractional scaling in the future, but at the moment only integer scale values will work.
```
output "eDP-1" {
scale 2.0
}
```
### `transform`
Rotate the output counter-clockwise.
Valid values are: `"normal"`, `"90"`, `"180"`, `"270"`, `"flipped"`, `"flipped-90"`, `"flipped-180"` and `"flipped-270"`.
Values with `flipped` additionally flip the output.
```
output "HDMI-A-1" {
transform "90"
}
```
### `position`
Set the position of the output in the global coordinate space.
This affects directional monitor actions like `focus-monitor-left`, and cursor movement.
The cursor can only move between directly adjacent outputs.
> [!NOTE]
> Output scale and rotation has to be taken into account for positioning: outputs are sized in logical, or scaled, pixels.
> For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080, so to put another output directly adjacent to it on the right, set its x to 1920.
> If the position is unset or results in an overlap, the output is instead placed automatically.
```
output "HDMI-A-1" {
position x=1280 y=0
}
```
#### Automatic Positioning
Niri repositions outputs from scratch every time the output configuration changes (which includes monitors disconnecting and connecting).
The following algorithm is used for positioning outputs.
1. Collect all connected monitors and their logical sizes.
1. Sort them by their name. This makes it so the automatic positioning does not depend on the order the monitors are connected. This is important because the connection order is non-deterministic at compositor startup.
1. Try to place every output with explicitly configured `position`, in order. If the output overlaps previously placed outputs, place it to the right of all previously placed outputs. In this case, niri will also print a warning.
1. Place every output without explicitly configured `position` by putting it to the right of all previously placed outputs.
+142
View File
@@ -0,0 +1,142 @@
### Per-Section Documentation
You can find documentation for various sections of the config on these wiki pages:
* [`input {}`](./Configuration:-Input.md)
* [`output "eDP-1" {}`](./Configuration:-Outputs.md)
* [`binds {}`](./Configuration:-Key-Bindings.md)
* [`layout {}`](./Configuration:-Layout.md)
* [top-level options](./Configuration:-Miscellaneous.md)
* [`window-rule {}`](./Configuration:-Window-Rules.md)
* [`animations {}`](./Configuration:-Animations.md)
* [`debug {}`](./Configuration:-Debug-Options.md)
### Loading
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
If that file is missing, niri will create it with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl).
Please use the default configuration file as the starting point for your custom configuration.
The configuration is live-reloaded.
Simply edit and save the config file, and your changes will be applied.
This includes key bindings, output settings like mode, window rules, and everything else.
You can run `niri validate` to parse the config and see any errors.
To use a different config file path, pass it in the `--config` or `-c` argument to `niri`.
### Syntax
The config is written in [KDL].
#### Comments
Lines starting with `//` are comments; they are ignored.
Also, you can put `/-` in front of a section to comment out the entire section:
```
/-output "eDP-1" {
everything inside here
is ignored
}
```
#### Flags
Toggle options in niri are commonly represented as flags.
Writing out the flag enables it, and omitting it or commenting it out disables it.
For example:
```
// "Focus follows mouse" is enabled.
input {
focus-follows-mouse
// Other settings...
}
```
```
// "Focus follows mouse" is disabled.
input {
// focus-follows-mouse
// Other settings...
}
```
#### Sections
Most sections cannot be repeated. For example:
```
// This is valid: every section appears once.
input {
keyboard {
// ...
}
touchpad {
// ...
}
}
```
```
// This is NOT valid: input section appears twice.
input {
keyboard {
// ...
}
}
input {
touchpad {
// ...
}
}
```
Exceptions are, for example, sections that configure different devices by name:
```
output "eDP-1" {
// ...
}
// This is valid: this section configures a different output.
output "HDMI-A-1" {
// ...
}
// This is NOT valid: "eDP-1" already appeared above.
// It will either throw a config parsing error, or otherwise not work.
output "eDP-1" {
// ...
}
```
### Defaults
Omitting most of the sections of the config file will leave you with the default values for that section.
A notable exception is `binds {}`: they do not get filled with defaults, so make sure you do not erase this section.
### Breaking Change Policy
Configuration backwards compatibility follows the Rust / Cargo semantic versioning standards.
A patch release (i.e. niri 0.1.3 to 0.1.4) must not cause a parse error on a config that worked on the previous version.
A minor release (i.e. niri 0.1.3 to 0.2.0) *can* cause previously valid config files to stop parsing.
When niri reaches 1.0, a major release (i.e. niri 1.0 to 2.0) will be required to break config backwards compatibility.
Exceptions can be made for parsing bugs.
For example, niri used to accept multiple binds to the same key, but this was not intended and did not do anything (the first bind was always used).
A patch release changed niri from silently accepting this to causing a parsing failure.
This is not a blanket rule, I will consider the potential impact of every breaking change like this before deciding to carry on with it.
Keep in mind that the breaking change policy applies only to niri releases.
Commits between releases can and do occasionally break the config as new features are ironed out.
However, I do try to limit these, since several people are running git builds.
[KDL]: https://kdl.dev/
+369
View File
@@ -0,0 +1,369 @@
### Overview
Window rules let you adjust behavior for individual windows.
They have `match` and `exclude` directives that control which windows the rule should apply to, and a number of properties that you can set.
Window rules are processed in order of appearance in the config file.
This means that you can put more generic rules first, then override them for specific windows later.
For example:
```
// Set open-maximized to true for all windows.
window-rule {
open-maximized true
}
// Then, for Alacritty, set open-maximized back to false.
window-rule {
match app-id="Alacritty"
open-maximized false
}
```
> [!TIP]
> In general, you cannot "unset" a property in a later rule, only set it to a different value.
> Use the `exclude` directives to avoid applying a rule for specific windows.
Here are all matchers and properties that a window rule could have:
```
window-rule {
match title="Firefox"
match app-id="Alacritty"
match is-active=true
match is-focused=false
// Properties that apply once upon window opening.
default-column-width { proportion 0.75; }
open-on-output "eDP-1"
open-maximized true
open-fullscreen true
// Properties that apply continuously.
draw-border-with-background false
opacity 0.5
block-out-from "screencast"
// block-out-from "screen-capture"
min-width 100
max-width 200
min-height 300
max-height 300
}
```
### Window Matching
Each window rule can have several `match` and `exclude` directives.
In order for the rule to apply, a window needs to match *any* of the `match` directives, and *none* of the `exclude` directives.
```
window-rule {
// Match all Telegram windows...
match app-id=r#"^org\.telegram\.desktop$"#
// ...except the media viewer window.
exclude title="^Media viewer$"
// Properties to apply.
open-on-output "HDMI-A-1"
}
```
Match and exclude directives have the same syntax.
There can be multiple *matchers* in one directive, then the window should match all of them for the directive to apply.
```
window-rule {
// Match Firefox windows with Gmail in title.
match app-id="org.mozilla.firefox" title="Gmail"
}
window-rule {
// Match Firefox, but only when it is active...
match app-id=r#"^org\.mozilla\.firefox$"# is-active=true
// ...or match Telegram...
match app-id=r#"^org\.telegram\.desktop$"#
// ...but don't match the Telegram media viewer.
// If you open a tab in Firefox titled "Media viewer",
// it will not be excluded because it doesn't match the app-id
// of this exclude directive.
exclude app-id=r#"^org\.telegram\.desktop$"# title="Media viewer"
}
```
Let's look at the matchers in more detail.
#### `title` and `app-id`
These are regular expressions that should match anywhere in the window title and app ID respectively.
```
// Match windows with title containing "Mozilla Firefox",
// or windows with app ID containing "Alacritty".
window-rule {
match title="Mozilla Firefox"
match app-id="Alacritty"
}
```
Raw KDL strings can be helpful for writing out regular expressions:
```
window-rule {
exclude app-id=r#"^org\.keepassxc\.KeePassXC$"#
}
```
You can find the title and the app ID of the currently focused window by running `niri msg focused-window`.
> [!TIP]
> Another way to find the window title and app ID is to configure the `wlr/taskbar` module in [Waybar](https://github.com/Alexays/Waybar) to include them in the tooltip:
>
> ```json
> "wlr/taskbar": {
> "tooltip-format": "{title} | {app_id}",
> }
> ```
#### `is-active`
Can be `true` or `false`.
Matches active windows (same windows that have the active border / focus ring color).
Every workspace on the focused monitor will have one active window.
This means that you will usually have multiple active windows (one per workspace), and when you switch between workspaces, you can see two active windows at once.
```
window-rule {
match is-active=true
}
```
#### `is-focused`
Can be `true` or `false`.
Matches the window that has the keyboard focus.
Contrary to `is-active`, there can only be a single focused window.
Also, when opening a layer-shell application launcher or pop-up menu, the keyboard focus goes to layer-shell.
While layer-shell has the keyboard focus, windows will not match this rule.
```
window-rule {
match is-focused=true
}
```
### Window Opening Properties
These properties apply once, when a window first opens.
To be precise, they apply at the point when niri sends the initial configure request to the window.
#### `default-column-width`
Set the default width for the new window.
```
// Give Blender and GIMP some guaranteed width on opening.
window-rule {
match app-id="^blender$"
// GIMP app ID contains the version like "gimp-2.99",
// so we only match the beginning (with ^) and not the end.
match app-id="^gimp"
default-column-width { fixed 1200; }
}
```
#### `open-on-output`
Make the window open on a specific output.
If such an output does not exist, the window will open on the currently focused output as usual.
If the window opens on an output that is not currently focused, the window will not be automatically focused.
```
// Open Firefox and Telegram (but not its Media Viewer)
// on a specific monitor.
window-rule {
match app-id=r#"^org\.mozilla\.firefox$"#
match app-id=r#"^org\.telegram\.desktop$"#
exclude app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$"
open-on-output "HDMI-A-1"
}
```
#### `open-maximized`
Make the window open as a maximized column.
```
// Maximize Firefox by default.
window-rule {
match app-id=r#"^org\.mozilla\.firefox$"#
open-maximized true
}
```
#### `open-fullscreen`
Make the window open fullscreen.
```
window-rule {
open-fullscreen true
}
```
You can also set this to `false` to *prevent* a window from opening fullscreen.
```
// Make the Telegram media viewer open in windowed mode.
window-rule {
match app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$"
open-fullscreen false
}
```
### Dynamic Properties
These properties apply continuously to open windows.
#### `block-out-from`
You can block out windows from xdg-desktop-portal screencasts.
They will be replaced with solid black rectangles.
This can be useful for password managers or messenger windows, etc.
![Screenshot showing a window visible normally, but blocked out on OBS.](./img/block-out-from-screencast.png)
To preview and set up this rule, check the `preview-render` option in the debug section of the config.
> [!CAUTION]
> The window is **not** blocked out from third-party screenshot tools.
> If you open some screenshot tool with preview while screencasting, blocked out windows **will be visible** on the screencast.
The built-in screenshot UI is not affected by this problem though.
If you open the screenshot UI while screencasting, you will be able to select the area to screenshot while seeing all windows normally, but on a screencast the selection UI will display with windows blocked out.
```
// Block out password managers from screencasts.
window-rule {
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
match app-id=r#"^org\.gnome\.World\.Secrets$"#
block-out-from "screencast"
}
```
Alternatively, you can block out the window out of *all* screen captures, including third-party screenshot tools.
This way you avoid accidentally showing the window on a screencast when opening a third-party screenshot preview.
This setting will still let you use the interactive built-in screenshot UI, but it will block out the window from the fully automatic screenshot actions, such as `screenshot-screen` and `screenshot-window`.
The reasoning is that with an interactive selection, you can make sure that you avoid screenshotting sensitive content.
```
window-rule {
block-out-from "screen-capture"
}
```
> [!WARNING]
> Be careful when blocking out windows based on a dynamically changing window title.
>
> For example, you might try to block out specific Firefox tabs like this:
>
> ```
> window-rule {
> // Doesn't quite work! Try to block out the Gmail tab.
> match app-id=r#"^org\.mozilla\.firefox$"# title="- Gmail "
>
> block-out-from "screencast"
> }
> ```
>
> It will work, but when switching from a sensitive tab to a regular tab, the contents of the sensitive tab **will show up on a screencast** for an instant.
>
> This is because window title (and app ID) are not double-buffered in the Wayland protocol, so they are not tied to specific window contents.
> There's no robust way for Firefox to synchronize visibly showing a different tab and changing the window title.
#### `opacity`
Set the opacity of the window.
`0.0` is fully transparent, `1.0` is fully opaque.
This is applied on top of the window's own opacity, so semitransparent windows will become even more transparent.
Opacity is applied to every surface of the window individually, so subsurfaces and pop-up menus will show window content behind them.
![Screenshot showing Adwaita Demo with a semitransparent pop-up menu.](./img/opacity-popup.png)
Also, focus ring and border with background will show through semitransparent windows (see `prefer-no-csd` and the `draw-border-with-background` window rule below).
```
// Make inactive windows semitransparent.
window-rule {
match is-active=false
opacity 0.95
}
```
#### `draw-border-with-background`
Override whether the border and the focus ring draw with a background.
Set this to `true` to draw them as solid colored rectangles even for windows which agreed to omit their client-side decorations.
Set this to `false` to draw them as borders around the window even for windows which use client-side decorations.
This property can be useful for rectangular windows that do not support the xdg-decoration protocol.
| With Background | Without Background |
| --------------- | ------------------ |
| ![](./img/simple-egl-border-with-background.png) | ![](./img/simple-egl-border-without-background.png) |
```
window-rule {
draw-border-with-background false
}
```
#### Size Overrides
You can amend the window's minimum and maximum size in logical pixels.
Keep in mind that the window itself always has a final say in its size.
These values instruct niri to never ask the window to be smaller than the minimum you set, or to be bigger than the maximum you set.
> [!NOTE]
> `max-height` will only apply to automatically-sized windows if it is equal to `min-height`.
> Either set it equal to `min-height`, or change the window height manually after opening it with `set-window-height`.
>
> This is a limitation of niri's window height distribution algorithm.
```
window-rule {
min-width 100
max-width 200
min-height 300
max-height 300
}
```
```
// Fix OBS with server-side decorations missing a minimum width.
window-rule {
match app-id=r#"^com\.obsproject\.Studio$"#
min-width 876
}
```
+11
View File
@@ -0,0 +1,11 @@
These are some of the general principles for the design of niri's window layout. They can be sidestepped in specific circumstances if there's a good reason.
1. Opening a new window should not affect the sizes of any existing windows.
2. The focused window should not move around on its own.
- In particular: windows opening, closing and resizing to the left of the focused window should not cause it to visually move.
3. If a window or popup is larger than the screen, it should be aligned on the top left corner.
- The top left area of a window is more likely to contain something important so it should always be visible.
4. Setting window width or height to a fixed pixel size (e.g. `set-column-width 1280` or `default-column-width { fixed 1280; }`) will set the size of the window itself, however setting to a proportional size (e.g. `set-column-width 50%`) will set the size of the tile, including the border added by niri.
- With proportions, the user is looking to tile multiple windows on screen, so they should include borders.
- With fixed sizes, the user wants to test a specific client size or take a specifically sized screenshot, so they should affect the window directly.
- After the size is set, it is always converted to a value that includes the borders, to make the code sane. That is, `set-column-width 1000` followed by changing the niri border width will resize the window accordingly.
+65
View File
@@ -0,0 +1,65 @@
## Running a Local Build
The main way of testing niri during development is running it as a nested window. The second step is usually switching to a different TTY and running niri there.
Once a feature or fix is reasonably complete, you generally want to run a local build as your main compositor for proper testing. The easiest way to do that is to install niri normally (from a distro package for example), then overwrite the binary with `sudo cp ./target/release/niri /usr/bin/niri`. Do make sure that you know how to revert to a working version in case everything breaks though.
If you use an RPM-based distro, you can generate an RPM package for a local build with `cargo generate-rpm`.
## Logging Levels
Niri uses [`tracing`](https://lib.rs/crates/tracing) for logging. This is how logging levels are used:
- `error!`: programming errors and bugs that are recoverable. Things you'd normally use `unwrap()` for. However, when a Wayland compositor crashes, it brings down the entire session, so it's better to recover and log an `error!` whenever reasonable. If you see an `ERROR` in the niri log, that always indicates a *bug*.
- `warn!`: something bad but still *possible* happened. Informing the user that they did something wrong, or that their hardware did something weird, falls into this category. For example, config parsing errors should be indicated with a `warn!`.
- `info!`: the most important messages related to normal operation. Running niri with `RUST_LOG=niri=info` should not make the user want to disable logging altogether.
- `debug!`: less important messages related to normal operation. Running niri with `debug!` messages hidden should not negatively impact the UX.
- `trace!`: everything that can be useful for debugging but is otherwise too spammy or performance intensive. `trace!` messages are *compiled out* of release builds.
## Tests
We have some unit tests, most prominently for the layout code and for config parsing.
When adding new operations to the layout, add them to the `Op` enum at the bottom of `src/layout/mod.rs` (this will automatically include it in the randomized tests), and if applicable to the `every_op` arrays below.
When adding new config options, include them in the config parsing test.
### Running Tests
Make sure to run `cargo test --all` to run tests from sub-crates too.
Some tests are a bit too slow to run normally, like the randomized tests of the layout code, so they are normally skipped. Set the `RUN_SLOW_TESTS` variable to run them:
```
env RUN_SLOW_TESTS=1 cargo test --all
```
It also usually helps to run the randomized tests for a longer period, so that they can explore more inputs. You can control this with environment variables. This is how I usually run tests before pushing:
```
env RUN_SLOW_TESTS=1 PROPTEST_CASES=200000 PROPTEST_MAX_GLOBAL_REJECTS=200000 RUST_BACKTRACE=1 cargo test --release --all
```
### Visual Tests
The `niri-visual-tests` sub-crate is a GTK application that runs hard-coded test cases so that you can visually check that they look right. It uses mock windows with the real layout and rendering code. It is especially helpful when working on animations.
## Profiling
We have integration with the [Tracy](https://github.com/wolfpld/tracy) profiler which you can enable by building niri with a feature flag:
```
cargo build --release --features=profile-with-tracy
```
Then you can open Tracy (you will need the latest stable release) and attach to a running niri instance to collect profiling data. This is **not** currently "on-demand" (until the next Tracy release), so niri will always collect profiling data when compiled this way, and you can't run a build like this as your main compositor.
To make a niri function show up in Tracy, instrument it like this:
```rust
pub fn some_function() {
let _span = tracy_client::span!("some_function");
// Code of the function.
}
```
+75
View File
@@ -0,0 +1,75 @@
When starting niri from a display manager like GDM, or otherwise through the `niri-session` binary, it runs as a systemd service.
This provides the necessary systemd integration to run programs like `mako` and services like `xdg-desktop-portal` bound to the graphical session.
Here's an example on how you might set up [`mako`](https://github.com/emersion/mako), [`waybar`](https://github.com/Alexays/Waybar), [`swaybg`](https://github.com/swaywm/swaybg) and [`swayidle`](https://github.com/swaywm/swayidle) to run as systemd services with niri.
In contrast to the `spawn-at-startup` config option, this lets you easily monitor their status and output, and restart or reload them.
1. Install them, i.e. `sudo dnf install mako waybar swaybg swayidle`
2. Create a `niri.service.wants` folder: `mkdir -p ~/.config/systemd/user/niri.service.wants`
This is a special systemd folder.
Any services linked there will be started together with `niri.service` (which is a systemd unit used by niri when running as a session).
3. `mako` and `waybar` provide systemd units out of the box, so you can simply symlink them into the `niri.service.wants` folder:
```
ln -s /usr/lib/systemd/user/mako.service ~/.config/systemd/user/niri.service.wants/
ln -s /usr/lib/systemd/user/waybar.service ~/.config/systemd/user/niri.service.wants/
```
4. `swaybg` does not provide a systemd unit, since you need to pass the background image as a command-line argument.
So we will make our own.
Put the following into `~/.config/systemd/user/swaybg.service`:
```
[Unit]
PartOf=graphical-session.target
After=graphical-session.target
Requisite=graphical-session.target
[Service]
ExecStart=/usr/bin/swaybg -m fill -i "%h/Pictures/LakeSide.png"
Restart=on-failure
```
Replace the image path with the one you want.
`%h` is expanded to your home directory.
After editing `swaybg.service`, run `systemctl --user daemon-reload` so systemd picks up the changes in the file.
Now, also symlink this to `niri.service.wants`:
```
ln -s ~/.config/systemd/user/swaybg.service ~/.config/systemd/user/niri.service.wants/
```
5. `swayidle` similarly does not provide a service so we will also make our own. Put the following into `~/.config/systemd/user/swayidle.service`:
```
[Unit]
PartOf=graphical-session.target
After=graphical-session.target
Requisite=graphical-session.target
[Service]
ExecStart=/usr/bin/swayidle -w timeout 601 'niri msg action power-off-monitors' timeout 600 'swaylock -f' before-sleep 'swaylock -f'
Restart=on-failure
```
Then, run `systemctl --user daemon-reload` and symlink this file to `niri.service.wants`:
```
ln -s ~/.config/systemd/user/swayidle.service ~/.config/systemd/user/niri.service.wants/
```
That's it!
Now these three utilities will be started together with the niri session and stopped when it exits.
You can also restart them with a command like `systemctl --user restart waybar.service`, for example after editing their config files.
### Running Programs Across Logout
When running niri as a session, exiting it (logging out) will kill all programs that you've started within. However, sometimes you want a program, like `tmux`, `dtach` or similar, to persist in this case. To do this, run it in a transient systemd scope:
```
systemd-run --user --scope tmux new-session
```
+5
View File
@@ -0,0 +1,5 @@
Welcome to the niri wiki!
Check out the available pages on the right.
The wiki is open to contribution, but please discuss bigger changes in [our Matrix room](https://matrix.to/#/#niri:matrix.org) first! The wiki is generated from files in the `wiki/` folder of the repository, so you can open a pull request modifying it there.
+12
View File
@@ -0,0 +1,12 @@
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.
> [!TIP]
> If you're getting parsing errors from `niri msg` after upgrading niri, make sure that you've restarted niri itself.
> You might be trying to run a newer `niri msg` against an older `niri` compositor.
+35
View File
@@ -0,0 +1,35 @@
Since niri is not a complete desktop environment, you will very likely want to run the following software to make sure that other apps work fine.
### Notification Daemon
Many apps need one. For example, [mako](https://github.com/emersion/mako) works well. Use [a systemd setup](./Example-systemd-Setup.md) or `spawn-at-startup`.
### Portals
These provide a cross-desktop API for apps to use for various things like file pickers or UI settings. Flatpak apps in particular require working portals.
Portals **require** [running niri as a session](https://github.com/YaLTeR/niri#session), which means through the `niri-session` script or from a display manager. You will want the following portals installed:
* `xdg-desktop-portal-gtk`: implements most of the basic functionality, this is the "default fallback portal".
* `xdg-desktop-portal-gnome`: required for screencasting support.
* `gnome-keyring`: implements the Secret portal, required for certain apps to work.
Then systemd should start them on-demand automatically. These particular portals are configured in `niri-portals.conf` which [must be installed](https://github.com/YaLTeR/niri#installation) in the correct location.
Since we're using `xdg-desktop-portal-gnome`, Flatpak apps will read the GNOME UI settings. For example, to enable the dark style, run:
```
dconf write /org/gnome/desktop/interface/color-scheme '"prefer-dark"'
```
### Authentication Agent
Required when apps need to ask for root permissions. Something like `plasma-polkit-agent` works fine. Start it [with systemd](./Example-systemd-Setup.md) or with `spawn-at-startup`.
Note that to start `plasma-polkit-agent` with systemd on Fedora, you'll need to override its systemd service to add the correct dependency. Run:
```
systemctl --user edit --full plasma-polkit-agent.service
```
Then add `After=graphical-session.target`.
+5
View File
@@ -0,0 +1,5 @@
Things to keep in mind with layer-shell components (bars, launchers, etc.):
1. Popups (tooltips, popup menus) render on the same layer as the component itself. Put your bar at the top layer, or menus will render below windows.
2. Components on the bottom and background layers will never receive keyboard focus, including for popups. They will however receive pointer focus as expected.
3. When a full-screen window is active and covers the entire screen, it will render above the top layer, and it will be prioritized for keyboard focus. If your launcher uses the top layer, and you try to run it while looking at a full-screen window, it won't show up. Only the overlay layer will show up on top of full-screen windows.
+48
View File
@@ -0,0 +1,48 @@
X11 is very cursed, so built-in Xwayland support is not planned at the moment.
However, there are multiple solutions to running X11 apps in niri.
## Directly running Xwayland in rootful mode
This method involves invoking XWayland directly and running it as its own window, it also requires an extra X11 window manager running inside it.
![Xwayland running in rootful mode.](https://github.com/YaLTeR/niri/assets/1794388/b64e96c4-a0bb-4316-94a0-ff445d4c7da7)
Here's how you do it:
1. Run `Xwayland` (just the binary on its own without flags).
This will spawn a black window which you can resize and fullscreen (with Mod+Shift+F) for convenience.
On older Xwayland versions the window will be screen-sized and non-resizable.
1. Run some X11 window manager in there, e.g. `env DISPLAY=:0 i3`.
This way you can manage X11 windows inside the Xwayland instance.
1. Run an X11 application there, e.g. `env DISPLAY=:0 flatpak run com.valvesoftware.Steam`.
With fullscreen game inside a fullscreen Xwayland you get pretty much a normal gaming experience.
One caveat is that currently rootful Xwayland doesn't seem to share clipboard with the compositor.
For textual data you can do it manually using [wl-clipboard](https://github.com/bugaevc/wl-clipboard), for example:
- `env DISPLAY=:0 xsel -ob | wl-copy` to copy from Xwayland to niri clipboard
- `wl-paste | env DISPLAY=:0 xsel -ib` to copy from niri to Xwayland clipboard
## Using the Cage Wayland compositor
It is also possible to run the X11 application in [Cage](https://github.com/cage-kiosk/cage), which runs a nested Wayland session which also supports Xwayland, where the X11 application can run in.
Compared to the Xwayland rootful method, this does not require running an extra X11 window manager, and can be used with one command `cage -- /path/to/application`. However, it can also cause issues if multiple windows are launched inside Cage, since Cage is meant to be used in kiosks, every new window will be automatically full-screened and take over the previously opened window.
To use Cage you need to:
1. Install `cage`, it should be in most repositories.
2. Run `cage -- /path/to/application` and enjoy your X11 program on niri.
Optionally one can also modify the desktop entry for the application and add the `cage --` prefix to the `Exec` property. The Spotify Flatpak for example would look something like this:
```ini
[Desktop Entry]
Type=Application
Name=Spotify
GenericName=Online music streaming service
Comment=Access all of your favorite music
Icon=com.spotify.Client
Exec=cage -- flatpak run com.spotify.Client
Terminal=false
```
+22
View File
@@ -0,0 +1,22 @@
## Usage
* [Example systemd Setup](./Example-systemd-Setup.md)
* [Important Software](./Important-Software.md)
* [LayerShell Components](./Layer%E2%80%90Shell-Components.md)
* [`niri msg`](./IPC.md)
* [VSCode, Chromium, WezTerm](./Application-Issues.md)
* [Xwayland](./Xwayland.md)
## Configuration
* [Overview](./Configuration:-Overview.md)
* [Input](./Configuration:-Input.md)
* [Outputs](./Configuration:-Outputs.md)
* [Key Bindings](./Configuration:-Key-Bindings.md)
* [Layout](./Configuration:-Layout.md)
* [Miscellaneous](./Configuration:-Miscellaneous.md)
* [Window Rules](./Configuration:-Window-Rules.md)
* [Animations](./Configuration:-Animations.md)
* [Debug Options](./Configuration:-Debug-Options.md)
## Development
* [Design Principles](./Design-Principles.md)
* [Developing niri](./Developing-niri.md)
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65b22b833dc0b05166b4b3403b1c6323315ca0885d4a75d2bbb68250a3855e96
size 432355
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc0f46cdc5fc56a483f56a052b934b02f915f8b95cce3a9309e2ce7bf02478d4
size 2623
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee21b6ed445f069270a6b0023a0c15659ac62cf7c92721792a3e3781148f2394
size 4608
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:164e445914282dc0e6e345f0b76ae59b617dfb8db6cf0c8b05cae51ed7d51b97
size 31700
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb4e3e64bb0402b402b945b778c39b083512f7453d1503b789b63c8d6ff69330
size 29540
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d1e456ff87be58fcda542473dcbb5c5d63bef4f95b7e71bc1790de5e012f640d
size 15837
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db034a4d42c382df3ff8aab1178f1978e1d7a20ff2318145c28a7f87e710eeaa
size 4999
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2623c4f1b11d82b0588893c87908b14337f42bd2ee328d17d1db74acf9c7bd17
size 15729
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:32e84ae4c14683d07da42bea96a66fbb3427b11ba7619a98d3dc73d77c9f999c
size 20959