Compare commits

...

520 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
Ivan Molodetskikh acd33653b3 README: Update screenshot 2024-03-09 14:45:18 +04:00
Ivan Molodetskikh f7c6516da7 README: Expand package listing 2024-03-09 14:29:20 +04:00
Ivan Molodetskikh b220420fba README: mention just "Touchpad gestures"
We've got both directions now.
2024-03-09 08:29:51 +04:00
Ivan Molodetskikh bbeaba16a0 Bump version to 0.1.3 2024-03-09 08:28:48 +04:00
Ivan Molodetskikh 9d7c39b89a Reposition outputs after potentially changing mode
Currently outputs aren't repositioned again after a mode change, which
can cause overlaps.
2024-03-09 08:23:57 +04:00
Ivan Molodetskikh 03fe864d07 Add xdg-foreign 2024-03-08 17:08:58 +04:00
Ivan Molodetskikh e45dbb8ef6 Pass through subpixel layout 2024-03-08 17:06:46 +04:00
Ivan Molodetskikh 5c4b71a5a4 Update Smithay and dependencies 2024-03-08 17:06:35 +04:00
Ivan Molodetskikh 348690afb6 Add wp-viewporter
Doesn't hurt I guess.
2024-03-08 16:52:54 +04:00
sodiboo ca22e70cc4 Implement wlr-screencopy v1 (#243)
* Implement wlr-screencopy

* Finish the implementation

Lots of changes, mainly to fix transform handling. Turns out, grim
expects transformed buffers and untransforms them by itself using info
from wl_output. This means that render helpers needed to learn how to
actually render transformed buffers.

Also, it meant that y_invert is no longer needed.

Next, moved the rendering to the Screencopy frame handler. Turns out,
copy() is more or less expected to return immediately, whereas
copy_with_damage() is expected to wait until the next VBlank. At least
that's the intent I parse reading the protocol.

Finally, brought the version from 3 down to 1, because
copy_with_damage() will need bigger changes. Grim still works, others
not really, mainly because they bind v3 unnecessarily, even if they
don't use the damage request.

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-03-08 04:10:55 -08:00
Ivan Molodetskikh 1a784e6e66 Remove NOTIFY_FD after reading it 2024-03-06 23:06:39 +04:00
Ivan Molodetskikh 3ee2db71a4 CI: Check dinit feature 2024-03-06 21:01:10 +04:00
Ivan Molodetskikh cedfd4944c Adjust comments 2024-03-06 21:01:00 +04:00
metent 431f070481 Add dinit support (#246)
* Add dinit support

- Add --notify-fd cli flag for ready notifications
- Set dinit activation environment when "dinit" feature flag is enabled

* Make systemd and dinit environment activation additive

* Use NOTIFY_FD env variable instead of --notify-fd cli flag for sending ready notifications

* Format with rustfmt
2024-03-06 08:57:43 -08:00
Ivan Molodetskikh 9cbbffc23c Improve spring comments in default config 2024-03-05 19:06:21 +04:00
Ivan Molodetskikh c6a1398d51 Add fuzzel to generate-rpm requires 2024-03-05 13:40:57 +04:00
Ivan Molodetskikh f9127616b0 Implement rubber banding for the vertical gesture 2024-03-05 13:32:57 +04:00
Ivan Molodetskikh ae89b2e514 Implement spring animations 2024-03-05 13:32:52 +04:00
Ivan Molodetskikh 732f7f6f33 animation: Apply slowdown in realtime 2024-03-05 13:23:06 +04:00
Ivan Molodetskikh 8bebd54c6d tile: Prepare for oscillating animations 2024-03-05 10:33:25 +04:00
Ivan Molodetskikh 1978e5b0b8 monitor: Handle switch idx < 0 and >= len 2024-03-05 08:43:20 +04:00
Ivan Molodetskikh 60b02545f3 Move animation to subfolder 2024-03-04 16:01:57 +04:00
Ivan Molodetskikh 2750b2038b Catch panics from edid-rs
Work around an integer overflow.

See: https://github.com/YaLTeR/niri/issues/239
2024-03-03 19:56:52 +04:00
Ivan Molodetskikh c4145b014a Add proper support for center = always in the horizontal gesture 2024-03-03 12:10:25 +04:00
Ivan Molodetskikh 2e51efd3a3 Remake horizontal gesture to snap with inertia 2024-03-03 09:33:00 +04:00
Ivan Molodetskikh caea05433e Extract WORKSPACE_GESTURE_MOVEMENT constant 2024-03-03 09:25:27 +04:00
Ivan Molodetskikh e4f78c26f0 swipe-tracker: Rename retain_recent to trim_history 2024-03-03 08:04:55 +04:00
Ivan Molodetskikh 1548db56ce Fix vertical gesture constant
400 is for width not height.
2024-03-02 21:18:02 +04:00
Ivan Molodetskikh 5f416abcf9 Change horizontal gesture to focus furthest window 2024-03-02 15:48:54 +04:00
Ivan Molodetskikh 66c1272420 Use unaccelerated delta for vertical gesture
With inertia in place it's ready for this.
2024-03-02 15:41:56 +04:00
Ivan Molodetskikh e0ec6e5b11 Make vertical touchpad swipe inertial
Values and implementation are heavily inspired by AdwSwipeTracker.
2024-03-02 14:33:22 +04:00
Ivan Molodetskikh 93243d7772 Disentangle frame callback sequence from real DRM sequence
It can currently happen that the estimated VBlank timer fires right
before a real VBlank, which can cause some sequence collisions, which
might cause frame callbacks to never be sent. To prevent this, just
track the frame callback sequence fully separately. There isn't really
any harm in this, and if we accidentally increment it more frequently
than necessary then nothing terrible will happen.
2024-03-02 08:20:17 +04:00
sodiboo 24537ec2ba Correctly handle parsing of Binds and DefaultColumnWidth (#234)
* add dev dependencies to flake

* parse only one default-column-width

* require exactly one action per bind, and unique keys for binds

* use proper filename for config errors if possible

* fix duplicate keybinds after invalid action, lose some sanity
2024-03-01 03:50:49 -08:00
Ivan Molodetskikh 88ac16c99a tty: Bump sequence on successful queue_frame()
Before this commit:

- niri queues frame
- successful VBlank happens, sequence is bumped, frame callbacks are
  sent
- niri receives commit, redraws, queues next frame, tries to send frame
  callbacks, but there wasn't a new VBlank yet, so the sequence is old,
  and frame callbacks aren't sent
- frame callbacks are sent only next VBlank
2024-03-01 12:56:55 +04:00
Ivan Molodetskikh 0add457cf0 tty: Avoid zero estimated vblank timer 2024-03-01 08:27:44 +04:00
Ivan Molodetskikh 6e5426ef22 Fix center-column regression
Mistake introduced along with the horizontal gesture.
2024-03-01 08:09:03 +04:00
Ivan Molodetskikh 202406aadf Fix presentation feedback panic with zero presentation time 2024-03-01 07:55:09 +04:00
Ivan Molodetskikh 92d9c7ff4f Add emulate-zero-presentation-time debug flag 2024-03-01 07:54:58 +04:00
Ivan Molodetskikh 28977d1d3f Move workspace gesture into monitor & fix missing workspace cleanup 2024-02-29 09:51:49 +04:00
Ivan Molodetskikh ba10bab010 Implement horizontal touchpad swipe 2024-02-29 09:51:49 +04:00
Ivan Molodetskikh 55038b7c07 Pass prev_idx explicitly to animate_view_offset_to_column() 2024-02-29 08:30:46 +04:00
Ivan Molodetskikh 8018839f5d Extract animate_view_offset_to_column() 2024-02-28 17:23:03 +04:00
Ivan Molodetskikh 077f22edd6 Append _fit to animate_view_offset_to_column() 2024-02-28 17:21:08 +04:00
Ivan Molodetskikh 4f7c3300ef Upgrade dependencies 2024-02-28 13:45:12 +04:00
Ivan Molodetskikh 5628bf7d77 Update Smithay 2024-02-28 13:23:15 +04:00
Christian Meissl 719697179f input: add basic touch support 2024-02-28 13:19:41 +04:00
Christian Meissl 5ac350d51c chore: update smithay 2024-02-28 13:19:41 +04:00
Ivan Molodetskikh 494e98c123 Parse CSS colors in {in,}active-color 2024-02-26 09:14:35 +04:00
Ivan Molodetskikh ec156a8587 Add environment {} config section 2024-02-24 10:08:56 +04:00
Ivan Molodetskikh e278e871c3 Expand ~ in spawn 2024-02-24 09:16:44 +04:00
Ivan Molodetskikh ab9d1aab4e Add open-fullscreen window rule 2024-02-24 08:44:21 +04:00
Ivan Molodetskikh 506dcd99d7 Handle un-/fullscreen after initial configure 2024-02-23 17:47:12 +04:00
Ivan Molodetskikh dfbc024127 Rename surface -> toplevel 2024-02-23 17:40:30 +04:00
Ivan Molodetskikh eb2dce1b53 Fix default width fixed not being honored with borders 2024-02-23 14:40:56 +04:00
Ivan Molodetskikh f5b776a947 Fix unset default width causing a window resize right away 2024-02-23 14:31:35 +04:00
Ivan Molodetskikh 6a587245eb Add open-maximized window rule 2024-02-23 14:24:39 +04:00
Ivan Molodetskikh 2317021a7c Implement explicit unmapped window state tracking 2024-02-23 14:01:32 +04:00
Ivan Molodetskikh af6485cd8c Fix new warnings 2024-02-22 14:04:18 +04:00
Ivan Molodetskikh f32a25eefe Improve shader formatting 2024-02-22 10:21:38 +04:00
Ivan Molodetskikh aefbad0cf7 Simplify gradient border shader 2024-02-22 10:17:06 +04:00
Ivan Molodetskikh b091202d86 visual-tests: Add gradient angle and area tests 2024-02-22 08:54:35 +04:00
Ivan Molodetskikh 48f0f6fb3c Implement gradient borders 2024-02-21 22:15:21 +04:00
Ivan Molodetskikh 340bac0690 Remove unnecessary crop bounds during workspace switch 2024-02-21 21:41:12 +04:00
Ivan Molodetskikh d1b8134337 focus-ring: Store config instead of individual fields 2024-02-21 20:54:24 +04:00
Ivan Molodetskikh 646e3d8995 Accept location in FocusRing
Makes it work more like other elements.
2024-02-21 11:08:48 +04:00
Ivan Molodetskikh d1fe6930a7 Move UI elements into submodule 2024-02-21 10:50:30 +04:00
Ivan Molodetskikh 9e60b344d0 Move watcher to utils 2024-02-21 10:45:03 +04:00
Ivan Molodetskikh 2c01cde9be Move spawn to submodule 2024-02-21 10:42:21 +04:00
Ivan Molodetskikh cb9dc9c0cd Move utils to subfolder 2024-02-21 10:33:09 +04:00
Ivan Molodetskikh 73d2807b4b Fix move_window_to_output losing window instead 2024-02-21 09:39:32 +04:00
Ivan Molodetskikh 7d41f113cb Change non-bug error! to warn!
Be consistent with our usage.
2024-02-21 09:20:34 +04:00
Ivan Molodetskikh 63e5cf8798 Add missing qualified path 2024-02-21 09:12:42 +04:00
Ivan Molodetskikh 9ce19ad7de Use niri_render_elements! for the screenshot UI 2024-02-21 09:12:40 +04:00
Ivan Molodetskikh 751f79dc35 Comment out toggle-debug-tint default bind 2024-02-21 07:58:23 +04:00
Ivan Molodetskikh b8aa0a86e7 Fix debug tint desync for new outputs 2024-02-21 07:58:23 +04:00
Ivan Molodetskikh 82fffdea80 Fix locking with DPMS-inactive monitors
This both enables locking while monitors are powered off (they have no
buffer attached at that point on a TTY, so no sensitive content can
become visible), and fixes the condition below to check even if the
rendering was skipped.
2024-02-21 07:40:50 +04:00
Ivan Molodetskikh 5b3bfd95d9 Upgrade logs about removing env vars to warn!
These are more visible now with the --session flag.
2024-02-21 07:27:49 +04:00
Ivan Molodetskikh 1a15aa704d ci: Check individual features 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh d58a45a96c Add systemd feature flag for systemd-specific things 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh 9f1b4ee299 Set XDG_CURRENT_DESKTOP and XDG_SESSION_TYPE from niri itself 2024-02-21 07:27:49 +04:00
Ivan Molodetskikh f0a5e9c933 Add --session CLI flag instead of detection based on systemd service
Allows running without systemd.
2024-02-21 07:27:49 +04:00
Ivan Molodetskikh c4c07841d7 niri.service: Put into session.slice
Now that we're separating spawned processes, put ourselves in the more
important session.slice.
2024-02-20 12:49:52 +04:00
Ivan Molodetskikh 6ba24e341f utils/spawn: Put processes into systemd scopes
This separates them from the niri scope for the purposes of e.g. the OOM
killer only killing the app and not the compositor.
2024-02-20 12:49:52 +04:00
Ivan Molodetskikh 13b6c74cc3 utils/spawn: Receive grandchild PID 2024-02-20 12:49:52 +04:00
Ivan Molodetskikh d8fb8d5ef0 Update for Smithay MultiGpu shadow copies 2024-02-18 21:12:07 +04:00
Ivan Molodetskikh 2b5eeb6162 Fix fullscreen handling before initial configure 2024-02-18 10:20:34 +04:00
Ivan Molodetskikh 85be5f746c default-config: Clarify how indexed workspace access works 2024-02-17 21:01:10 +04:00
Ivan Molodetskikh dd7362913e Ignore mouse releases for dismissing overlays 2024-02-17 14:07:51 +04:00
Ivan Molodetskikh 62892d6361 Prevent locking while another lock client is already active
Fixes double swaylock from manual + swayidle.
2024-02-17 07:47:06 +04:00
Ivan Molodetskikh 31c13b6a69 default-config: Document enable-color-transformations-capability debug flag 2024-02-17 07:23:43 +04:00
Ivan Molodetskikh baaac2f3c4 Update Smithay 2024-02-16 22:40:37 +04:00
Ivan Molodetskikh 3fdefae45b Bump version to 0.1.2 2024-02-16 18:00:19 +04:00
Ivan Molodetskikh 6345224e95 default-config: Fix spelling mistakes
Ok I added automatic :set spell for KDL now.
2024-02-16 17:40:18 +04:00
Ivan Molodetskikh b3d2096439 Replace set_modified() with manual impl
MSRV moment
2024-02-16 08:46:58 +04:00
Ivan Molodetskikh 94ded2f6a9 CI: Add a MSRV job 2024-02-16 08:33:19 +04:00
Ivan Molodetskikh fa3bc69f94 Add watcher tests 2024-02-15 10:31:53 +04:00
Viktor Pocedulic 363e1d8764 input: enable configuring of trackpoint devices 2024-02-15 10:27:12 +04:00
Ivan Molodetskikh 8e1d4de0dc tty: Filter out interlaced modes
They don't seem to work. wlroots also filters them:
https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/feb54979c0940655e36119c63e18a9ee72cc03b0/backend/drm/drm.c#L1461
2024-02-14 21:14:01 +04:00
Ivan Molodetskikh 72e3fadb9a default-config: Specify example refresh rate with 3 digits
This is the format you need to use.
2024-02-14 19:55:31 +04:00
Ivan Molodetskikh 78cda2e67f tty: Truncate Edid strings to nul
Otherwise they crash in wayland-rs when converting to CString.
2024-02-14 19:49:34 +04:00
Ivan Molodetskikh 924e21f69b Focus output unconditionally after moving window there
Fixes output not getting focus if there was no window to move.
2024-02-14 09:06:13 +04:00
Ivan Molodetskikh befdebfa03 Add the beginnings of window rules 2024-02-14 08:32:14 +04:00
Ivan Molodetskikh 7960a73e9d config: Fix missing layout {} defaulting to 0 gaps 2024-02-13 17:47:11 +04:00
Ivan Molodetskikh 749ee5d627 Do initial configuration right before sending initial configure
Let the toplevel fill in some details about itself.
2024-02-13 17:47:11 +04:00
Ivan Molodetskikh 952dd48115 Deduplicate call to miette hook 2024-02-13 12:16:58 +04:00
Ivan Molodetskikh cbd066ab68 default-config: Document animation properties 2024-02-12 20:46:29 +04:00
Ivan Molodetskikh bccde351fb Update flake.lock 2024-02-12 09:58:04 +04:00
Kiara Grouwstra beaffb1b97 CI: check nix build works 2024-02-12 09:57:34 +04:00
Shawn Wallace 385454378b Implement DRM leasing
Closes #178
2024-02-12 09:48:54 +04:00
Ivan Molodetskikh 18f06a7acd Fix border getting default values for focus ring 2024-02-12 09:34:54 +04:00
Ivan Molodetskikh 6e23073019 Move default_border() into FocusRing 2024-02-12 09:22:22 +04:00
Ivan Molodetskikh a9fcbf81eb Export NIRI_SOCKET to systemd/dbus environment 2024-02-12 08:56:39 +04:00
Ivan Molodetskikh a99f34cba8 tty: Activate monitors on session resume 2024-02-12 08:45:45 +04:00
Ivan Molodetskikh bd2277fa25 tty: Notify idle activity on session resume 2024-02-12 08:42:34 +04:00
Ivan Molodetskikh 67182129ff Add skip-confirmation flag to the quit action 2024-02-12 07:53:48 +04:00
Ivan Molodetskikh d6b116d229 Add missing space 2024-02-12 07:53:48 +04:00
Ivan Molodetskikh c20a843ab2 Add log message when confirming exit dialog 2024-02-12 07:53:48 +04:00
Kiara Grouwstra 1b752fe08f exclude visual tests from nix, closes #181 2024-02-12 00:01:03 +04:00
Ivan Molodetskikh 89f74aae98 freedesktop-screensaver: Filter out non-interesting messages 2024-02-11 23:05:37 +04:00
Ivan Molodetskikh 5e553c2679 Implement org.freedesktop.ScreenSaver Inhibit
xdg-desktop-portal currently has no way of disabling the Inhibit portal
or ever returning an error to the application from it. Thus Flatpak
Firefox will never fall back to its Wayland backend. To remedy this,
let's actually implement the FDO Inhibit interface that the portal can
use.
2024-02-11 22:26:59 +04:00
Ivan Molodetskikh cabf712821 hotkey-overlay: Deduplicate Spawn actions 2024-02-11 09:27:34 +04:00
Ivan Molodetskikh 0931447ec1 Implement error reporting in IPC 2024-02-11 09:19:37 +04:00
Ivan Molodetskikh a388c25795 Update dependencies 2024-02-10 15:01:34 +04:00
Ivan Molodetskikh 5c4d9824a4 Remove logind-zbus dependency
It isn't updated and we don't really need it anyway.
2024-02-10 14:58:22 +04:00
Ivan Molodetskikh ca4ee5ae25 hotkey-overlay: Only show Spawn binds with Mod/Super 2024-02-10 14:37:38 +04:00
Ivan Molodetskikh 93e16a6582 Implement niri msg action 2024-02-10 09:40:32 +04:00
Ivan Molodetskikh 3486fa5536 Remove unused directories workspace dep 2024-02-10 09:34:35 +04:00
Ivan Molodetskikh c022d74c82 Remove extra `` in comment 2024-02-10 09:19:08 +04:00
Ivan Molodetskikh e68641c0a7 Move CLI types to submodule 2024-02-10 08:40:13 +04:00
Ivan Molodetskikh 2a892ef511 input: Fix Clippy warning 2024-02-10 08:38:19 +04:00
Ivan Molodetskikh 90c6721e97 config: Add missing Smithay feature
Fixes build on nightly.
2024-02-10 07:51:53 +04:00
Ivan Molodetskikh e5cd9e9307 default-config: Replace Mod with Super in swaylock bind
Otherwise it conflicts with Mod+L in nested.
2024-02-09 16:23:33 +04:00
Ivan Molodetskikh 573dca10cc input: Fix handling of binds with compositor mod but no explicit Mod 2024-02-09 16:23:05 +04:00
Ivan Molodetskikh 577fba82e5 input: Split bound_action() and add tests 2024-02-09 16:16:18 +04:00
Ivan Molodetskikh b9116c579a Implement idle-notify and idle-inhibit 2024-02-09 15:50:40 +04:00
Ivan Molodetskikh d8dcadc5b2 Clamp animation slowdown to sane values 2024-02-07 20:03:23 +04:00
Ivan Molodetskikh 6424a2738d Make all animations configurable 2024-02-07 17:14:24 +04:00
Ivan Molodetskikh 753a90430a animation: Accept ms as u32
Less boilerplate elsewhere.
2024-02-07 16:32:38 +04:00
Ivan Molodetskikh f9085db564 Implement window open animations 2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 49ce791d13 Add a Tracy span to OffscreenRenderElement::new 2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 4b8e04da04 Activate the new right_of window on its workspace
This way when a dialog opens on a different workspace, the user will see
it right away when they switch to that workspace.
2024-02-07 13:16:54 +04:00
Ivan Molodetskikh 026ad8f377 Add a way to override the element ID for primary output check 2024-02-07 11:30:52 +04:00
Ivan Molodetskikh 0761401650 Add OffscreenRenderElement 2024-02-07 11:30:33 +04:00
Ivan Molodetskikh 3360517f62 Clear before rendering to texture
Otherwise I see artifacts on some GTK dialogs.
2024-02-07 11:18:55 +04:00
Ivan Molodetskikh 9896fd67a0 Open dialogs to the right of their parent, don't steal focus 2024-02-07 10:49:01 +04:00
Ivan Molodetskikh 15ec699fbb visual-tests: Remove "Just" prefix 2024-02-07 09:24:41 +04:00
Ivan Molodetskikh a1cc39a437 visual-tests/tile: Disable focus ring 2024-02-07 09:22:00 +04:00
Ivan Molodetskikh 738d9a2b40 Add blank line 2024-02-06 19:53:31 +04:00
Ivan Molodetskikh 68752db51b layout: Add Column::advance_animations() 2024-02-06 19:52:47 +04:00
Ivan Molodetskikh d4929b8e18 Inline variable 2024-02-06 19:52:10 +04:00
Ivan Molodetskikh 93c547f749 Move focus ring into Tile
For now, will make the open animation better.
2024-02-06 19:49:51 +04:00
Ivan Molodetskikh e2b91c0c1c layout: Fix refresh in tests
Didn't affect anything but still.
2024-02-06 19:09:27 +04:00
Ivan Molodetskikh 322b5cbac7 Add Layout::with_options() 2024-02-06 19:09:15 +04:00
Ivan Molodetskikh 592791611a Change render functions to accept iterators 2024-02-06 17:53:25 +04:00
Ivan Molodetskikh d073d2ab3d Move render functions to render_helpers 2024-02-06 17:53:25 +04:00
Ivan Molodetskikh b2298db5c5 Split render_helpers.rs 2024-02-06 11:25:25 +04:00
Ivan Molodetskikh baa6263cbe Bump libinput to 1.21, add dwtp flag 2024-02-06 09:54:46 +04:00
Ivan Molodetskikh 795da53d53 README: Update Ubuntu dependencies 2024-02-06 09:49:53 +04:00
Ivan Molodetskikh 122afff7d1 Add niri-visual-tests 2024-02-06 09:40:45 +04:00
Ivan Molodetskikh d2a4e6a0cb Update dependencies 2024-02-06 09:40:34 +04:00
Ivan Molodetskikh 8916b18c6b Run Ubuntu CI in a 23.10 container
We will soon need newer dependencies.
2024-02-06 09:40:32 +04:00
Ivan Molodetskikh b0d0fce5f3 Move use into feature-gated function 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 3dc4a5fdac Fix Clippy warnings 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 1706a46b2b layout: Mark some things as pub 2024-02-05 17:40:16 +04:00
Ivan Molodetskikh 3789d85588 Add lib.rs, become a mixed lib-bin crate
Will be used for visual tests.
2024-02-05 17:40:16 +04:00
Dennis Ranke 3a23417e98 Add consume-or-expel-window-left/right commands 2024-02-05 14:09:47 +04:00
Ivan Molodetskikh 6bb83757ee Convert everything to niri_render_elements! {} 2024-02-05 14:05:08 +04:00
Ivan Molodetskikh b62a07956a Add niri_render_elements! {}
We will be using this in several other places.
2024-02-05 13:55:09 +04:00
Ivan Molodetskikh 96016790b2 layout: Replace with_tiles_in_render_order() with Iterator 2024-02-05 13:55:09 +04:00
Ivan Molodetskikh bf978fe98d layout/tile: Return Iterator of render elements
Avoid a Vec.
2024-02-05 13:55:09 +04:00
Ivan Molodetskikh 57521c69c3 layout: Add TileRenderElement 2024-02-04 22:52:11 +04:00
Ivan Molodetskikh da826e42aa layout: Add LayoutElementRenderElement
Allows for testing layout rendering without Wayland windows.
2024-02-04 22:31:44 +04:00
Ivan Molodetskikh b824cf90ab layout: Generalize traversal between rendering and input 2024-02-04 22:10:26 +04:00
Ivan Molodetskikh 7a4bb8ba8a layout: Make rendering not Window-specific
Doesn't need to be any more.
2024-02-04 21:23:00 +04:00
Ivan Molodetskikh 72c8f569ac Bump version to 0.1.1 2024-02-03 10:00:06 +04:00
Ivan Molodetskikh 798d9c55df Support fullscreen for new windows 2024-02-03 09:45:26 +04:00
Ivan Molodetskikh 05613eed1e Verify that pending fullscreen matches column 2024-02-03 09:44:34 +04:00
Ivan Molodetskikh b23dd4b800 Respect natural-scroll for workspace switch gesture 2024-02-03 09:00:08 +04:00
Ivan Molodetskikh 1f72089a46 Place new workspace after current when moving
This feels more natural, also makes moving back and forth idempotent in
most cases.
2024-02-03 08:42:56 +04:00
Ivan Molodetskikh fbe9020915 Update dependencies 2024-02-02 17:04:17 +04:00
Ivan Molodetskikh 2036116f16 config: Premultiply alpha in Color when converting to f32
Smithay wants premultiplied alpha.
2024-02-01 18:53:45 +04:00
Ivan Molodetskikh 9afd728ae9 Add error messages to backend initialization 2024-02-01 16:55:46 +04:00
Andreas Stührk e51268a39e Add actions to move the active workspace to another monitor 2024-02-01 12:29:46 +04:00
Ivan Molodetskikh 0a715ce155 default-config: Improve wording for focus-ring/border comment
SSD or server-side decorations is never mentioned elsewhere.
2024-02-01 12:06:13 +04:00
Ivan Molodetskikh 89ac958670 default-config: Document how focus ring and border draw behind
Related: https://github.com/YaLTeR/niri/issues/150
2024-02-01 10:08:15 +04:00
Ivan Molodetskikh 2e50f8dee0 Hardcode winit transform for now 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 7052f0129e Stop screencasts on size changes 2024-01-31 23:02:38 +04:00
axtloss 962e159db6 Add option to rotate outputs 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 11bff3a2f1 Update Smithay (rotation fix) 2024-01-31 23:02:38 +04:00
Ivan Molodetskikh 15606304f2 README: Bring AUR link back 2024-01-30 22:36:30 -08:00
Christian Meissl 85eac9d9d0 chore: bump smithay
includes fixes for wrong direct scan-out transform
and damage artifacts on output transform changes.
also includes a fix for a race in popup surface re-use.
2024-01-30 15:30:31 +04:00
Ivan Molodetskikh d3f4583c90 foreign_toplevel: Use OutputHandler to send output_enter on demand 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh fefb1cccd6 foreign_toplevel: Update the focused window last 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh deef52519a foreign_toplevel: Change activated to mean keyboard focus 2024-01-30 12:30:57 +04:00
Ivan Molodetskikh 59ff331597 Implement wlr-foreign-toplevel-management
The parent event isn't sent but whatever.
2024-01-30 12:30:57 +04:00
Christian Meissl b813f99abd tty: reset surface state after changing monitor state
changing the "ACTIVE" property of a surface requires
to re-evaluate the surface state.
2024-01-30 08:03:21 +04:00
Ivan Molodetskikh d9b9cec8b8 README: Remove AUR link for now
It doesn't work properly yet apparently.
2024-01-29 12:29:32 -08:00
Christian Meissl 597ea62d17 input: update keyboard led state 2024-01-28 23:43:08 +04:00
Ivan Molodetskikh 51243a0a50 Show notification about creating a default config 2024-01-28 17:15:47 +04:00
Ivan Molodetskikh 0ebcc3e0d6 Create default config file if missing 2024-01-28 17:15:33 +04:00
Ivan Molodetskikh 64c85d865e winit: Don't remove output on CloseRequested
More winit events can process after CloseRequested, which will cause a
panic if trying to access the now-removed output.
2024-01-28 16:30:29 +04:00
Ivan Molodetskikh 367e4955ea Mark Msg as pub
Seems to break the build on 1.72.0 otherwise.
2024-01-28 09:34:42 +04:00
Ivan Molodetskikh dd967554d1 Bump version to 0.1.0 2024-01-27 14:10:31 +04:00
Ivan Molodetskikh 6d7c220137 Try harder to find an output for the screenshot UI
The mouse might be outside any outputs, let's try to open in that case
anyway.
2024-01-27 14:09:55 +04:00
Ivan Molodetskikh d77aac1afa Fix damage when rendering to texture 2024-01-27 10:50:40 +04:00
Ivan Molodetskikh 837a0a20fb Update README 2024-01-25 08:34:42 +04:00
Ivan Molodetskikh ecdf756b55 Name output render element better 2024-01-25 08:02:33 +04:00
Christian Meissl 73f3c160b2 use pixman for cursor plane rendering 2024-01-25 07:49:51 +04:00
Christian Meissl 5f99eb13ab Remove hack for fixed EGLDisplay issue 2024-01-25 07:49:51 +04:00
Christian Meissl 20326b093c Update smithay 2024-01-25 07:49:51 +04:00
Ivan Molodetskikh 467d92a4b4 github: Add a feature request link to start a discussion 2024-01-23 17:41:35 +04:00
Ivan Molodetskikh 15bb69c0b9 Update issue templates 2024-01-23 05:36:19 -08:00
Ivan Molodetskikh adfbfdffb3 Create a bug report template 2024-01-23 05:34:38 -08:00
Ivan Molodetskikh 087ed260c5 Update Smithay (find_popup_root_surface() panic fix) 2024-01-23 17:12:47 +04:00
Ivan Molodetskikh f5642ab733 Ignore popup grabs when IME keyboard grab is active
Doing this properly will require more refactors, potentially in Smithay.
For now let's just ignore popup grabs to make popups work.
2024-01-23 17:05:08 +04:00
Ivan Molodetskikh ab9706cb30 screencast: Emit MonitorsChanged 2024-01-23 12:02:52 +04:00
Ivan Molodetskikh 05f2a3709b srceencast: Send stream size
Kooha requires this (even though it's optional). Unfortunately, Kooha
also seems to want memfd recording so it doesn't work anyway.
2024-01-23 11:36:11 +04:00
Ivan Molodetskikh 743173ef64 config: Bump precision on the default widths
This seems to actually matter on my 2560x display.
2024-01-22 20:43:33 +04:00
Ivan Molodetskikh cbbb7a26fc Update Smithay, use device changed session resume code
Should fix most cases of monitors failing to light up after a TTY
switch.
2024-01-22 16:13:39 +04:00
sodiboo 18566e3366 Watch for canonical filename, not just mtime 2024-01-22 07:42:45 +04:00
Ivan Molodetskikh df48337d83 tty: Delay output config update until resume
We can't do anything while paused.
2024-01-21 10:25:39 +04:00
Ivan Molodetskikh f5e9b40140 tty: Check changes against pending connectors and mode
If we queued some DRM changes, they will be in pending. Also be more
resilient by removing unwrap.
2024-01-21 10:24:42 +04:00
Ivan Molodetskikh 5cacd03e85 Return error instead of broken screenshot for portal 2024-01-21 10:03:13 +04:00
Ivan Molodetskikh 6945ccde18 Bump version to 0.1.0-beta.1 2024-01-20 09:38:42 +04:00
Ivan Molodetskikh e86e9c6c9a CI: Add a Fedora build 2024-01-20 09:25:50 +04:00
Ivan Molodetskikh dc47de178f Add an option to skip the hotkey overlay at startup 2024-01-20 08:31:05 +04:00
Ivan Molodetskikh 65e864965e Print git version in clap too 2024-01-19 20:46:10 +04:00
Ivan Molodetskikh 55ad36addc layout: Fix crash due to workspace transfer during switch 2024-01-19 20:24:59 +04:00
Ivan Molodetskikh 26c8cbb961 layout: Fix crash due to workspace cleanup during switch 2024-01-19 20:24:18 +04:00
Ivan Molodetskikh 031133c052 README: Add link to important software wiki page 2024-01-19 07:01:56 -08:00
Ivan Molodetskikh a6f821d3fa Update dependencies 2024-01-19 09:41:16 +04:00
Ivan Molodetskikh 475b3df2b5 Don't crash when failing to render a cursor
I only hit this when the renderer was completely busted, but
nevertheless.
2024-01-19 09:13:32 +04:00
Ivan Molodetskikh 1541835f00 Prettify Return => Enter key 2024-01-19 08:35:36 +04:00
Ivan Molodetskikh 4b9cb2f0d3 Add exit confirmation dialog 2024-01-19 08:33:54 +04:00
Ivan Molodetskikh 3461c66d2c Redraw upon starting PW stream
Otherwise it may take a while for the first frame to arrive.
2024-01-18 21:16:36 +04:00
Ivan Molodetskikh 011c91c98a Add an important hotkeys overlay 2024-01-18 20:32:44 +04:00
Ivan Molodetskikh edafa139f6 portal: Name and sort monitors, fix session restore
xdp-gnome restores by a combination of model + make + serial. We
currently can't set those reliably (until libdisplay-info most monitors
will have them unknown) so pass the connector name instead. This will
work as expected in most cases.
2024-01-18 16:31:04 +04:00
Ivan Molodetskikh fa9b3ed106 Add a config parse error notification
We can't rely on a notification daemon being available, especially
during initial niri setup. So, render our own.
2024-01-18 12:44:05 +04:00
Ivan Molodetskikh cc62a403c0 Update Smithay (deadlock fix) 2024-01-18 11:14:39 +04:00
Ivan Molodetskikh 0f85c79548 Watch config path even if it didn't exist at startup 2024-01-18 11:13:36 +04:00
Ivan Molodetskikh 6beef26662 Fix dependency sorting 2024-01-18 11:00:49 +04:00
Ivan Molodetskikh 616055e205 Update README.md 2024-01-17 03:15:05 -08:00
Ivan Molodetskikh 40c85da102 Add an IPC socket and a niri msg outputs subcommand 2024-01-17 10:45:18 +04:00
Ivan Molodetskikh 768b326028 Rename connectors to enabled_outputs 2024-01-17 10:25:23 +04:00
Ivan Molodetskikh f068157f55 Add a calloop futures executor 2024-01-17 10:24:01 +04:00
Ivan Molodetskikh 6703d5ce72 tty: Add Tracy span to on_output_config_changed() 2024-01-17 10:21:40 +04:00
Ivan Molodetskikh 12590f689a Write a comment on xdg-decoration lack of live-reload 2024-01-16 20:43:28 +04:00
Ivan Molodetskikh 4656332d07 Add live-reload to libinput settings 2024-01-16 20:29:37 +04:00
Ivan Molodetskikh 954f711bf3 Extract apply_libinput_settings() 2024-01-16 20:28:37 +04:00
Ivan Molodetskikh c09c964420 default-config: Add example for spawn with bash 2024-01-16 20:08:31 +04:00
Ivan Molodetskikh 1f9abaaa58 Add live-reload for output mode 2024-01-16 18:02:30 +04:00
Ivan Molodetskikh eb4946c3d8 tty: Extract pick_mode() 2024-01-16 18:01:25 +04:00
Ivan Molodetskikh 5f440f7be3 Add live-reload for output on/off 2024-01-16 15:34:00 +04:00
Ivan Molodetskikh 6644cc16ff tty: Remove connector arg from connector_disconnected() 2024-01-16 15:33:37 +04:00
Ivan Molodetskikh 9e667efc4c Close layer surfaces upon output removal
Fixes https://github.com/YaLTeR/niri/issues/23
2024-01-16 13:28:29 +04:00
Ivan Molodetskikh 8a7e4bc3cd Add Tracy span to Config::load and parse 2024-01-16 12:53:40 +04:00
Ivan Molodetskikh 69907f123d Add live-reload of output scales 2024-01-16 11:34:34 +04:00
Ivan Molodetskikh 6ca3b6ddb5 Move output scale setting into niri 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh fc5a080ca5 layout: Fix surface leaving output when consuming into column 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh 83719a49b7 Add live-reload of output positions 2024-01-16 09:46:02 +04:00
Ivan Molodetskikh da4967d43c Reposition all outputs on any change
This way the positioning is independent of the order of plugging in.
2024-01-16 08:43:28 +04:00
Ivan Molodetskikh d958a9679c Change message from debug to trace 2024-01-16 07:38:52 +04:00
Ivan Molodetskikh e4643c6dbe Implement security-context, hide some protocols from it 2024-01-15 16:02:07 +04:00
Ivan Molodetskikh 59763fd0da Hide decoration globals when we need CSD
This gets the current SDL2 with libdecor working.
2024-01-15 16:01:01 +04:00
Ivan Molodetskikh 533659eef8 Update Smithay 2024-01-15 15:59:36 +04:00
Ivan Molodetskikh 81443d8e16 Change default binds to move columns instead of windows 2024-01-15 11:51:04 +04:00
Ivan Molodetskikh fb38ae26c9 Add move-column-to-monitor* binds
As opposed to move-window-to-monitor*
2024-01-15 10:36:59 +04:00
Ivan Molodetskikh cc4acdf24a Add move-column-to-workspace* binds
As opposed to move-window-to-workspace*
2024-01-15 10:31:44 +04:00
Ivan Molodetskikh 2506d43bb9 xdg-decoration: Document SDL2 bug 2024-01-14 09:28:03 +04:00
Ivan Molodetskikh d899bc4712 Revert "Be more insistent on CSD by default"
This reverts commit 43e2cf14d2.

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

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

* Describe nix flake in readme

* Add `niri-config` to build source list

* Add maintainer info

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

* Clarify Nix/NixOS README instructions

* Shorten Nix/NixOS build instructions

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

* Move NixOS installation instruction to "Tip" section

---------

Co-authored-by: Bryce Berger <bryce.z.berger@gmail.com>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-01-10 22:43:46 -08:00
Ivan Molodetskikh b2df3e104f Document debug settings in the default config 2024-01-09 08:18:34 +04:00
Ivan Molodetskikh ec2d339a86 Add panic subcommand to check backtraces 2024-01-09 08:08:38 +04:00
Ivan Molodetskikh 629a2ccb47 layout: Improve Options randomization in tests 2024-01-08 20:57:53 +04:00
Thomas Versteeg fb93038bd8 Add center-focused-column setting 2024-01-08 17:37:18 +04:00
Ivan Molodetskikh 71fef2ad2e Add a few mouse libinput settings 2024-01-08 11:53:34 +04:00
Ivan Molodetskikh c6841f19e9 Add touchpad tap-button-map setting 2024-01-08 10:32:04 +04:00
Ivan Molodetskikh e1971c4af5 Add touchpad dwt setting 2024-01-08 10:24:00 +04:00
Ivan Molodetskikh 07b1d0e98d Add touchpad accel-profile setting 2024-01-08 10:23:53 +04:00
Ivan Molodetskikh ffe25f5cc4 Update Smithay and dependencies 2024-01-07 17:44:08 +04:00
Ivan Molodetskikh 43e2cf14d2 Be more insistent on CSD by default
By default I want CSD so that people don't necessarily have to learn all
of the shortcuts right away, and can at least use the X to close
windows.
2024-01-07 09:41:56 +04:00
Ivan Molodetskikh 2c59131f7f Remove debuginfo from niri-config
Finally, the file sizes are back to sane.
2024-01-07 09:28:50 +04:00
Ivan Molodetskikh 64c41fa2c8 Move config into a separate crate
Get miette and knuffel deps contained within.
2024-01-07 09:28:14 +04:00
Ivan Molodetskikh 4e0aa39113 [cfg-breaking] Move layout settings into their own scope 2024-01-06 13:04:21 +04:00
Ivan Molodetskikh dcb80efc88 Bump version to 0.1.0-alpha.3 2024-01-06 09:30:46 +04:00
Ivan Molodetskikh 3d5de921cd Add render-drm-device debug config option 2024-01-06 09:14:48 +04:00
Ivan Molodetskikh 8703feedee tty: Print full path to render node 2024-01-06 08:46:30 +04:00
Ivan Molodetskikh a27d22571d README: Link NixOS flake 2024-01-06 08:33:12 +04:00
Ivan Molodetskikh d10af92aea Redraw on toggling debug tint 2024-01-06 08:28:12 +04:00
Ivan Molodetskikh 0bc83eda71 Update Smithay and dependencies 2024-01-06 08:22:53 +04:00
Ivan Molodetskikh 6fce5662e7 Update Smithay 2024-01-05 23:23:32 +04:00
Ivan Molodetskikh 1c7c5b3f28 tty: Avoid non-linear cross-device formats 2024-01-05 23:23:32 +04:00
Ivan Molodetskikh b9d7812f1f Update dependencies 2024-01-05 23:23:32 +04:00
Ivan Molodetskikh 655b9808b9 Update Smithay 2024-01-05 23:23:30 +04:00
Ivan Molodetskikh 5cd31e5730 Implement multi-GPU support
Rendering always happens on the primary GPU.
2024-01-03 18:16:20 +04:00
Ivan Molodetskikh de3fc2def0 Make rendering generic over renderer 2024-01-03 18:05:15 +04:00
Ivan Molodetskikh fd1d4b07fd Replace renderer() with with_primary_renderer() 2024-01-03 11:38:09 +04:00
Ivan Molodetskikh 8b5acd5e6e Rename RenderResult::Error to Skipped 2024-01-03 11:38:07 +04:00
Ivan Molodetskikh 31bb9096e2 Move Niri::render() invocation to backends 2024-01-03 11:38:02 +04:00
Ivan Molodetskikh dae93ee159 Render again for screencast
Will be needed when multi-gpu support is added.
2024-01-03 11:37:04 +04:00
Ivan Molodetskikh 57a7347620 Move import_dmabuf to backends 2024-01-03 11:37:04 +04:00
Ivan Molodetskikh 628891db2c Add disable-cursor-plane debug flag 2024-01-03 08:42:49 +04:00
Ivan Molodetskikh be6e25f5fb tty: Extract config variable 2024-01-03 08:42:49 +04:00
Ivan Molodetskikh e005a795e7 layout: Make generic over Renderer 2024-01-01 17:31:58 +04:00
Ivan Molodetskikh 655fe413fb tty: Don't store render formats
Actually, how did it even fork before? Pretty sure it was storing render
formats, not texture formats, but with render formats
weston-simple-dmabuf-feedback doesn't work?
2024-01-01 17:31:58 +04:00
Ivan Molodetskikh ac6ff7ff41 tty: Disable dmabuf global before destroying 2024-01-01 17:31:58 +04:00
Ivan Molodetskikh 84befb4e91 tty: Move dmabuf_global up into Tty 2024-01-01 09:54:45 +04:00
Ivan Molodetskikh d39f7bebf3 tty: Fix import sorting
One of the imports was too long so rustfmt bailed.
2024-01-01 09:40:41 +04:00
Ivan Molodetskikh 0dd9a42087 Move DmabufState to Niri 2023-12-31 12:02:39 +04:00
Ivan Molodetskikh 658941f2c3 Enable EGL for the winit backend
More relevant now that mesa on Fedora is trying to force non-working
Zink for some reason.
2023-12-31 09:46:22 +04:00
Ivan Molodetskikh 6ccc4147ae Use remove/insert for columns instead of swapping
Now with move-to-first/last this is relevant.
2023-12-29 16:58:18 +04:00
Ivan Molodetskikh 46d5f5ec4d Add randomized border to proptest 2023-12-29 11:43:16 +04:00
Ivan Molodetskikh c64e96d0d8 Add test with i32::MAX max_size 2023-12-29 11:42:54 +04:00
Ivan Molodetskikh 44d3a5b9a2 Use saturating_sub in window_height_for_tile_height() 2023-12-29 11:38:01 +04:00
Ivan Molodetskikh 5d95de97a0 Add new binds to the README 2023-12-29 08:09:17 +04:00
Ivan Molodetskikh 56174b2c34 Add move-column-to-{first,last} 2023-12-29 08:01:02 +04:00
Ivan Molodetskikh 310aa2b464 layout: Extract move_column_to() 2023-12-29 08:00:40 +04:00
Ivan Molodetskikh d6c553091f Add focus-column-{first,last} 2023-12-29 07:51:14 +04:00
Ivan Molodetskikh 097c415036 layout: Use saturating_add() in several places
Apparently VSCode sends i32::MAX worth of max_size?
2023-12-28 18:53:08 +04:00
Ivan Molodetskikh 2d16c04869 Fix unfullscreening not always updating view offset 2023-12-28 10:49:31 +04:00
Ivan Molodetskikh 249f2b7a21 Add disable-power-key-handling option 2023-12-28 09:36:10 +04:00
Ivan Molodetskikh f3e5e13c45 Activate windows only on active monitors
This matches what we do in advance_animations().
2023-12-28 09:09:28 +04:00
Ivan Molodetskikh b13892ca63 Activate windows when clicking on the border 2023-12-28 08:45:12 +04:00
Ivan Molodetskikh 777ad4ee5c Rename window -> tile, fix popup unconstraining window_y
I missed the window_y thing and found it thanks to the rename. Too lazy
to split it into a commit.
2023-12-28 08:25:42 +04:00
Ivan Molodetskikh c21805bf70 layout: Refactor to support window decorations, add border and fullscreen backdrop
Windows are now wrapped in Tiles, which keep track of window-specific
decorations. Particularly, I implemented a black fullscreen backdrop,
which finally brings fullscreened windows smaller than the screen in
line with how the Wayland protocol says they should look—centered in a
black rectangle. I also implemented window borders, which are similar to
the focus ring, but always visible (and hence affect the layout and
sizing).
2023-12-27 21:51:42 +04:00
Ivan Molodetskikh bfc2418267 Make LayoutElement more visual-geometry-based 2023-12-26 17:40:25 +04:00
Ivan Molodetskikh 77b4715e0b Use correct type alias in FocusRing::render 2023-12-26 17:28:52 +04:00
Ivan Molodetskikh c048abc8b5 layout: Add Column::position 2023-12-26 10:29:00 +04:00
Ivan Molodetskikh 4dd7578fe7 layout: Use column.contains more 2023-12-25 11:06:03 +04:00
Ivan Molodetskikh ea72e4dae8 README: Add Xwayland wiki link 2023-12-24 19:41:23 +04:00
Ivan Molodetskikh 0c671ee493 Remove dependency of LayoutElement on SpaceElement 2023-12-24 18:18:44 +04:00
Ivan Molodetskikh 324c1efd04 Remove Clone bound from LayoutElement
It is no longer necessary.
2023-12-24 18:02:13 +04:00
Ivan Molodetskikh 99e75b95b7 Remove unnecessary window clone 2023-12-24 18:02:03 +04:00
Ivan Molodetskikh 489a225fae Add and use remove_window_by_idx 2023-12-24 18:00:46 +04:00
Ivan Molodetskikh 85cb4b42f6 Remove clones from active_window 2023-12-24 17:40:30 +04:00
Ivan Molodetskikh be2e551a89 Move clones up from find_window_and_output 2023-12-24 17:38:13 +04:00
Ivan Molodetskikh ed3080d908 Split layout mod into files
No functional change intended.
2023-12-24 15:10:09 +04:00
Ivan Molodetskikh 461ce5f363 Move output_size to utils 2023-12-24 14:30:19 +04:00
Ivan Molodetskikh 624c799ebf Move layout.rs into its own module 2023-12-24 14:20:50 +04:00
Ivan Molodetskikh 57f267454f Bump version to 0.1.0-alpha.2 2023-12-23 08:43:03 +04:00
Ivan Molodetskikh 86c4c1368e Implement pointer-constraints 2023-12-21 16:19:16 +04:00
Ivan Molodetskikh 17c23dc50f Update tablet cursor location higher up 2023-12-21 16:17:19 +04:00
Ivan Molodetskikh 5b1de86d33 Add configurable struts 2023-12-21 08:37:30 +04:00
Ivan Molodetskikh 58162ce685 Update Smithay
Popup positioner coordinate system fix.
2023-12-20 20:20:09 +04:00
Ivan Molodetskikh 9ac925ea0c Try unconstraining popups with padding first 2023-12-20 09:18:32 +04:00
Ivan Molodetskikh 0f83eacb42 Update dependencies 2023-12-19 21:06:49 +04:00
Ivan Molodetskikh e259061cbc Implement popup unconstraining
Using my new Smithay implementation.
2023-12-19 20:56:00 +04:00
Ivan Molodetskikh 206493bb35 Update Smithay 2023-12-19 20:48:15 +04:00
Ivan Molodetskikh c29a049245 Fix some cases of incomplete search for surface output
Most visibly, fixes screen not immediately redrawing upon layer-shell
popup commits.

There's still a number of places with questionable handling left, mostly
to do with subsurfaces (like, find_popup_root_surface() doesn't go up to
subsurfaces), and session-lock. I don't have good clients to test these.
2023-12-19 13:32:13 +04:00
Matt Cuneo d6b62ad09d Add optional fallback to workspace focus/move for window focus/move (#93)
* Add optional fallback to workspace focus/move for window focus/move commands

* Refactored to separate commands

* fix indentation

* fix white space

* Stylistic fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2023-12-19 00:25:05 -08:00
Ivan Molodetskikh d155f5cd6c Add a config flag to disable an output 2023-12-18 10:27:41 +04:00
Ivan Molodetskikh 74ff4f1903 Add a validate subcommand for config validation 2023-12-18 10:19:58 +04:00
Ivan Molodetskikh 8c3107af7b Make main() return Result
For reporting the config validation error.
2023-12-18 10:17:04 +04:00
Ivan Molodetskikh 8bcd18ace2 Move miette set earlier 2023-12-18 10:02:11 +04:00
Ivan Molodetskikh 4fefab7d6b Extract allowed action checks 2023-12-09 09:43:26 +04:00
Ivan Molodetskikh 675932c05b Document compute_tablet_position() 2023-12-09 09:30:56 +04:00
Ivan Molodetskikh 475d6e4be1 Extract tablet_seat and desc variables 2023-12-09 09:28:41 +04:00
Ivan Molodetskikh d9e27988a7 Extract tablet data variables 2023-12-09 09:25:27 +04:00
Ivan Molodetskikh 1be860c527 Add trace span to process_libinput_event 2023-12-09 09:23:41 +04:00
Ivan Molodetskikh b3e0a6c543 Remove extraneous full path 2023-12-09 09:23:25 +04:00
Ivan Molodetskikh 23a5bd3670 Extract input handlers to functions 2023-12-09 09:22:58 +04:00
Ivan Molodetskikh d397375d57 Move regular pointer to tablet pointer pos on proximity out 2023-12-08 08:32:42 +04:00
Ivan Molodetskikh cb3ba5105d Update dependencies 2023-12-08 08:01:52 +04:00
Ivan Molodetskikh 243519598e Live-reload keyboard config
This needed the Smithay bump for a deadlock fix.
2023-12-08 07:58:03 +04:00
Ivan Molodetskikh 0b5f232bc2 Update Smithay 2023-12-08 07:57:45 +04:00
Ivan Molodetskikh 9b3478a3d7 Prevent stealing focus from fullscreen clients
Got hit by that Syncthing disconnect dialog a few times while playing
games.
2023-12-05 15:28:31 +04:00
Ivan Molodetskikh cb1e5d6c19 Track tablet pointer separately, don't sent wl_pointer events
Tablets are not supposed to send wl_pointer events. This unbreaks GTK 4
clients for example.
2023-12-05 10:24:41 +04:00
Ivan Molodetskikh 11ae17b220 Extract to_xkb_config() to a method 2023-12-05 08:04:46 +04:00
Ivan Molodetskikh 40b633be5c Implement relative-pointer
Xwayland actually makes use of it, so I can finally verify that it
works!
2023-12-04 18:12:12 +04:00
Ivan Molodetskikh 0e29e7f6ff Keep monitor aspect ratio and clamp to monitor for tablets
Before, the full tablet area was used, even if the aspect ratio didn't
match the monitor. Also, the coordinates weren't clamped.
2023-12-03 13:50:07 +04:00
Ivan Molodetskikh 626c720b7a Set version for cargo-generate-rpm 2023-12-03 13:49:50 +04:00
Ivan Molodetskikh 3f76b71115 Add example systemd setup link to the README 2023-11-27 08:45:30 +04:00
Ivan Molodetskikh 1599a01f3b Add COPR link to README 2023-11-26 22:02:17 +04:00
119 changed files with 27086 additions and 7801 deletions
+1
View File
@@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text
+21
View File
@@ -0,0 +1,21 @@
---
name: Bug report
about: Report a bug or a crash
title: ''
labels: bug
assignees: ''
---
<!-- Please describe the issue here at the top, then fill in the system information below. -->
### System Information
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
* niri version:
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
* GPU:
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
* CPU:
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: Feature request
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
+124 -22
View File
@@ -24,6 +24,7 @@ jobs:
name: test - ${{ matrix.configuration }}
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -32,34 +33,90 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install stable --profile minimal
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.configuration }}
- name: Build (no default features)
run: cargo build ${{ matrix.release-flag }} --no-default-features
- name: Check (no default features)
run: cargo check ${{ matrix.release-flag }} --no-default-features
- name: Build
run: cargo build ${{ matrix.release-flag }}
- name: Check (just dbus)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dbus
- name: Check (just systemd)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features systemd
- name: Check (just dinit)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dinit
- name: Check (just xdp-gnome-screencast)
run: cargo check ${{ matrix.release-flag }} --no-default-features --features xdp-gnome-screencast
- name: Check
run: cargo check ${{ matrix.release-flag }}
- name: Build (with profiling)
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
- name: Build Tests
run: cargo test --no-run --all ${{ matrix.release-flag }}
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
- name: Test
run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
visual-tests:
strategy:
fail-fast: false
name: visual tests
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --package niri-visual-tests
msrv:
strategy:
fail-fast: false
name: 'msrv - 1.72.0'
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
- uses: dtolnay/rust-toolchain@1.72.0
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-targets
clippy:
strategy:
@@ -67,6 +124,7 @@ jobs:
name: clippy
runs-on: ubuntu-22.04
container: ubuntu:23.10
steps:
- uses: actions/checkout@v4
@@ -75,15 +133,12 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
- name: Install Rust
run: |
rustup set auto-self-update check-only
rustup toolchain install stable --profile minimal --component clippy
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
@@ -107,3 +162,50 @@ jobs:
- name: Run rustfmt
run: cargo fmt --all -- --check
fedora:
runs-on: ubuntu-22.04
container: fedora:39
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
- uses: Swatinem/rust-cache@v2
- run: cargo build --all
nix:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check flake inputs
uses: DeterminateSystems/flake-checker-action@v4
continue-on-error: true
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v3
continue-on-error: true
- 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
+1
View File
@@ -1 +1,2 @@
/target
/result
Generated
+1252 -516
View File
File diff suppressed because it is too large Load Diff
+84 -42
View File
@@ -1,48 +1,83 @@
[package]
name = "niri"
version = "0.1.0-alpha.1"
[workspace]
members = ["niri-visual-tests"]
[workspace.package]
version = "0.1.4"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
readme = "README.md"
repository = "https://github.com/YaLTeR/niri"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow = { version = "1.0.75" }
arrayvec = "0.7.4"
async-channel = { version = "2.1.0", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.1"
clap = { version = "4.4.8", features = ["derive"] }
directories = "5.0.1"
git-version = "0.3.8"
keyframe = { version = "1.1.1", default-features = false }
knuffel = "3.2.0"
libc = "0.2.150"
logind-zbus = { version = "3.1.2", optional = true }
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
miette = "5.10.0"
notify-rust = { version = "4.10.0", optional = true }
pipewire = { version = "0.7.2", optional = true }
png = "0.17.10"
portable-atomic = { version = "1.5.1", default-features = false, features = ["float"] }
profiling = "1.0.11"
sd-notify = "0.4.1"
serde = { version = "1.0.193", features = ["derive"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[workspace.dependencies]
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"] }
tracy-client = { version = "0.16.4", default-features = false }
url = { version = "2.5.0", optional = true }
xcursor = "0.3.4"
zbus = { version = "3.14.1", optional = true }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.17.0", default-features = false }
[dependencies.smithay]
[workspace.dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
default-features = false
[workspace.dependencies.smithay-drm-extras]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
[package]
name = "niri"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow.workspace = true
arrayvec = "0.7.4"
async-channel = { version = "2.2.0", optional = true }
async-io = { version = "1.13.0", optional = true }
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.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.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 }
png = "0.17.13"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.15"
sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.115"
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
xcursor = "0.3.5"
zbus = { version = "~3.15.2", optional = true }
[dependencies.smithay]
workspace = true
features = [
"backend_drm",
"backend_egl",
@@ -52,36 +87,42 @@ features = [
"backend_udev",
"backend_winit",
"desktop",
"libinput_1_19",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
]
[dependencies.smithay-drm-extras]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
[dev-dependencies]
proptest = "1.4.0"
proptest-derive = "0.4.0"
xshell = "0.2.5"
[features]
default = ["dbus", "xdp-gnome-screencast"]
# Enables DBus support (required for xdp-gnome and power button inhibiting).
dbus = ["zbus", "logind-zbus", "async-channel", "async-io", "notify-rust", "url"]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
dbus = ["zbus", "async-channel", "async-io", "notify-rust", "url"]
# Enables systemd integration (global environment, apps in transient scopes).
systemd = ["dbus"]
# Enables screencasting support through xdg-desktop-portal-gnome.
xdp-gnome-screencast = ["dbus", "pipewire"]
# Enables the Tracy profiler instrumentation.
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
# Enables dinit integration (global environment).
dinit = []
[profile.release]
debug = "line-tables-only"
overflow-checks = true
lto = "thin"
[profile.release.package.niri-config]
# knuffel with chomsky generates a metric ton of debuginfo.
debug = false
[package.metadata.generate-rpm]
version = "0.1.4"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
@@ -92,3 +133,4 @@ assets = [
]
[package.metadata.generate-rpm.requires]
alacritty = "*"
fuzzel = "*"
+85 -38
View File
@@ -1,56 +1,97 @@
# niri
<h1 align="center">niri</h1>
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=matrix"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
</p>
A scrollable-tiling Wayland compositor.
![](https://github.com/YaLTeR/niri/assets/1794388/2b246c2c-7cf3-4a11-96eb-ad0c7f2f4ed6)
![](https://github.com/YaLTeR/niri/assets/1794388/e35fd9e1-105b-4bd5-94c9-207fd6fb3c18)
## About
Windows are arranged in columns on an infinite strip going to the right.
Opening a new window never causes existing windows to resize.
Every monitor has its own separate window strip.
Windows can never "overflow" onto an adjacent monitor.
Workspaces are dynamic and arranged vertically.
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Features
- Scrollable tiling
- Dynamic workspaces like in GNOME
- Built-in screenshot UI
- Monitor screencasting through xdg-desktop-portal-gnome
- 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
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try.
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
Note that NVIDIA GPUs might have rendering issues.
## Idea
## Inspiration
Niri implements scrollable tiling, heavily inspired by [PaperWM].
Windows are arranged in columns on an infinite strip going to the right.
Every column takes up a full monitor worth of height, divided among its windows.
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
With multiple monitors, every monitor has its own separate window strip.
Windows can never "overflow" onto an adjacent monitor.
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
This is one of the reasons that prompted me to try writing my own compositor.
PaperWM is a solid implementation, but, being a GNOME Shell extension, it has to work around Shell's global window coordinate space to prevent windows from overflowing.
## Packages
Niri also has dynamic workspaces which work similar to GNOME Shell.
Since windows go left-to-right horizontally, workspaces are arranged vertically.
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
There are several community-maintained distribution packages that you can use to install niri.
Here are some of them:
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
- Fedora COPR (I maintain this one myself): https://copr.fedorainfracloud.org/coprs/yalter/niri/
- AUR: [niri](https://aur.archlinux.org/packages/niri), [niri-bin](https://aur.archlinux.org/packages/niri-bin), [niri-git](https://aur.archlinux.org/packages/niri-git)
- NixOS Flake: https://github.com/sodiboo/niri-flake
- FreeBSD Ports: https://www.freshports.org/x11-wm/niri
- Gentoo GURU: https://gpo.zugaina.org/Overlays/guru/gui-wm/niri
## Building
First, install the dependencies for your distribution.
- Ubuntu:
- Ubuntu 23.10:
```sh
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
sudo apt-get install -y gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
```
- Fedora:
```sh
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel clang
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
```
Next, build niri with `cargo build --release`.
Next, get latest stable Rust: https://rustup.rs/
Then, build niri with `cargo build --release`.
### NixOS/Nix
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
```
nix run --impure github:guibou/nixGL -- ./results/bin/niri
```
## Installation
@@ -84,10 +125,18 @@ Starting it from there will run niri as a desktop session.
The niri session will autostart apps through the systemd xdg-autostart target.
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
Niri also works with some parts of xdg-desktop-portal-gnome.
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.
## Configuration
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
When running on a TTY, the Mod key is <kbd>Super</kbd>.
@@ -97,6 +146,7 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| Hotkey | Description |
| ------ | ----------- |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
@@ -109,14 +159,16 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused window to the monitor to the side |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused column to the monitor to the side |
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused window to the workspace below |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused window to the workspace above |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused column to the workspace below |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
| <kbd>Mod</kbd><kbd>1</kbd><kbd>9</kbd> | Switch to a workspace by index |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd><kbd>9</kbd> | Move the focused window to a workspace by index |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd><kbd>9</kbd> | Move the focused column to a workspace by index |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
@@ -132,20 +184,15 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>T</kbd> | Toggle debug tinting of rendered elements |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
## Configuration
## Contact
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 many of the configuration settings, like key binds or gaps, as you change the config file.
Though, some settings are still missing live-reload support.
Notably, output modes and positions will only apply when the output is reconnected.
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
[PaperWM]: https://github.com/paperwm/PaperWM
[mako]: https://github.com/emersion/mako
[OBS]: https://flathub.org/apps/com.obsproject.Studio
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
Generated
+138
View File
@@ -0,0 +1,138 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1709610799,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "81c393c776d5379c030607866afef6406ca1be57",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1709274179,
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
"owner": "nix-community",
"repo": "fenix",
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1705332318,
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1709386671,
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1709219524,
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+106
View File
@@ -0,0 +1,106 @@
# This flake file is community maintained
# Maintainers:
# Bill Sun (github/billksun)
{
description = "Niri: A scrollable-tiling Wayland compositor.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
nix-filter.url = "github:numtide/nix-filter";
fenix = {
url = "github:nix-community/fenix/monthly";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
crane,
nix-filter,
flake-utils,
fenix,
...
}: let
systems = ["aarch64-linux" "x86_64-linux"];
in
flake-utils.lib.eachSystem systems (
system: let
pkgs = nixpkgs.legacyPackages.${system};
toolchain = fenix.packages.${system}.complete.toolchain;
craneLib = crane.lib.${system}.overrideToolchain toolchain;
craneArgs = {
pname = "niri";
version = self.rev or "dirty";
src = nixpkgs.lib.cleanSourceWith {
src = craneLib.path ./.;
filter = path: type:
(builtins.match "resources" path == null) ||
((craneLib.filterCargoSources path type) &&
(builtins.match "niri-visual-tests" path == null));
};
nativeBuildInputs = with pkgs; [
pkg-config
autoPatchelfHook
clang
gdk-pixbuf
graphene
gtk4
libadwaita
];
buildInputs = with pkgs; [
wayland
systemd # For libudev
seatd # For libseat
libxkbcommon
libinput
mesa # For libgbm
fontconfig
stdenv.cc.cc.lib
pipewire
pango
];
runtimeDependencies = with pkgs; [
wayland
mesa
libglvnd # For libEGL
];
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
};
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
in {
formatter = pkgs.alejandra;
checks.niri = niri;
packages.default = niri;
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
packages = niri.runtimeDependencies;
# Force linking to libEGL, which is always dlopen()ed, and to
# libwayland-client, which is always dlopen()ed except by the
# obscure winit backend.
RUSTFLAGS = map (a: "-C link-arg=${a}") [
"-Wl,--push-state,--no-as-needed"
"-lEGL"
"-lwayland-client"
"-Wl,--pop-state"
];
};
}
);
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "niri-config"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.6.2"
knuffel = "3.2.0"
miette = "5.10.0"
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
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "niri-ipc"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
clap = { workspace = true, optional = true }
serde.workspace = true
[features]
clap = ["dep:clap"]
+395
View File
@@ -0,0 +1,395 @@
//! Types for communicating with niri via IPC.
#![warn(missing_docs)]
use std::collections::HashMap;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
/// Name of the environment variable containing the niri IPC socket path.
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
/// Request from client to niri.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Request {
/// Request information about connected outputs.
Outputs,
/// Request information about the focused window.
FocusedWindow,
/// Perform an action.
Action(Action),
}
/// Reply from niri to client.
///
/// Every request gets one reply.
///
/// * If an error had occurred, it will be an `Reply::Err`.
/// * If the request does not need any particular response, it will be
/// `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`.
/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants.
pub type Reply = Result<Response, String>;
/// Successful response from niri to client.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Response {
/// A request that does not need a response was handled successfully.
Handled,
/// Information about connected outputs.
///
/// Map from connector name to output info.
Outputs(HashMap<String, Output>),
/// Information about the focused window.
FocusedWindow(Option<Window>),
}
/// Actions that niri can perform.
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
// variants from niri-config should be present here.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
pub enum Action {
/// Exit niri.
Quit {
/// Skip the "Press Enter to confirm" prompt.
#[cfg_attr(feature = "clap", arg(short, long))]
skip_confirmation: bool,
},
/// Power off all monitors via DPMS.
PowerOffMonitors,
/// Spawn a command.
Spawn {
/// Command to spawn.
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
command: Vec<String>,
},
/// Open the screenshot UI.
Screenshot,
/// Screenshot the focused screen.
ScreenshotScreen,
/// Screenshot the focused window.
ScreenshotWindow,
/// Close the focused window.
CloseWindow,
/// Toggle fullscreen on the focused window.
FullscreenWindow,
/// Focus the column to the left.
FocusColumnLeft,
/// Focus the column to the right.
FocusColumnRight,
/// Focus the first column.
FocusColumnFirst,
/// Focus the last column.
FocusColumnLast,
/// Focus the window below.
FocusWindowDown,
/// Focus the window above.
FocusWindowUp,
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceDown,
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp,
/// Move the focused column to the left.
MoveColumnLeft,
/// Move the focused column to the right.
MoveColumnRight,
/// Move the focused column to the start of the workspace.
MoveColumnToFirst,
/// Move the focused column to the end of the workspace.
MoveColumnToLast,
/// Move the focused window down in a column.
MoveWindowDown,
/// Move the focused window up in a column.
MoveWindowUp,
/// Move the focused window down in a column or to the workspace below.
MoveWindowDownOrToWorkspaceDown,
/// Move the focused window up in a column or to the workspace above.
MoveWindowUpOrToWorkspaceUp,
/// Consume or expel the focused window left.
ConsumeOrExpelWindowLeft,
/// Consume or expel the focused window right.
ConsumeOrExpelWindowRight,
/// Consume the window to the right into the focused column.
ConsumeWindowIntoColumn,
/// Expel the focused window from the column.
ExpelWindowFromColumn,
/// Center the focused column on the screen.
CenterColumn,
/// Focus the workspace below.
FocusWorkspaceDown,
/// Focus the workspace above.
FocusWorkspaceUp,
/// Focus a workspace by index.
FocusWorkspace {
/// Index of the workspace to focus.
#[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.
MoveWindowToWorkspaceUp,
/// Move the focused window to a workspace by index.
MoveWindowToWorkspace {
/// Index of the target workspace.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown,
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp,
/// Move the focused column to a workspace by index.
MoveColumnToWorkspace {
/// Index of the target workspace.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused workspace down.
MoveWorkspaceDown,
/// Move the focused workspace up.
MoveWorkspaceUp,
/// Focus the monitor to the left.
FocusMonitorLeft,
/// Focus the monitor to the right.
FocusMonitorRight,
/// Focus the monitor below.
FocusMonitorDown,
/// Focus the monitor above.
FocusMonitorUp,
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft,
/// Move the focused window to the monitor to the right.
MoveWindowToMonitorRight,
/// Move the focused window to the monitor below.
MoveWindowToMonitorDown,
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp,
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft,
/// Move the focused column to the monitor to the right.
MoveColumnToMonitorRight,
/// Move the focused column to the monitor below.
MoveColumnToMonitorDown,
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp,
/// Change the height of the focused window.
SetWindowHeight {
/// How to change the height.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Switch between preset column widths.
SwitchPresetColumnWidth,
/// Toggle the maximized state of the focused column.
MaximizeColumn,
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Switch between keyboard layouts.
SwitchLayout {
/// Layout to switch to.
#[cfg_attr(feature = "clap", arg())]
layout: LayoutSwitchTarget,
},
/// Show the hotkey overlay.
ShowHotkeyOverlay,
/// Move the focused workspace to the monitor to the left.
MoveWorkspaceToMonitorLeft,
/// Move the focused workspace to the monitor to the right.
MoveWorkspaceToMonitorRight,
/// Move the focused workspace to the monitor below.
MoveWorkspaceToMonitorDown,
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp,
/// Toggle a debug tint on windows.
ToggleDebugTint,
}
/// Change in window or column size.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
/// Set the size in logical pixels.
SetFixed(i32),
/// Set the size as a proportion of the working area.
SetProportion(f64),
/// Add or subtract to the current size in logical pixels.
AdjustFixed(i32),
/// Add or subtract to the current size as a proportion of the working area.
AdjustProportion(f64),
}
/// Layout to switch to.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutSwitchTarget {
/// The next configured layout.
Next,
/// The previous configured layout.
Prev,
}
/// Connected output.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Output {
/// Name of the output.
pub name: String,
/// Textual description of the manufacturer.
pub make: String,
/// Textual description of the model.
pub model: String,
/// Physical width and height of the output in millimeters, if known.
pub physical_size: Option<(u32, u32)>,
/// Available modes for the output.
pub modes: Vec<Mode>,
/// Index of the current mode in [`Self::modes`].
///
/// `None` if the output is disabled.
pub current_mode: Option<usize>,
/// 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.
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct Mode {
/// Width in physical pixels.
pub width: u16,
/// Height in physical pixels.
pub height: u16,
/// Refresh rate in millihertz.
pub refresh_rate: u32,
/// 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 {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err("trailing characters after '%' are not allowed");
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err("value is missing"),
}
}
None => {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err("value is missing"),
}
}
}
}
}
impl FromStr for LayoutSwitchTarget {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"next" => Ok(Self::Next),
"prev" => Ok(Self::Prev),
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
}
}
}
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""#
)),
}
}
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "niri-visual-tests"
version.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
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.4", path = ".." }
niri-config = { version = "0.1.4", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+14
View File
@@ -0,0 +1,14 @@
# niri-visual-tests
> [!NOTE]
>
> This is a development-only app, you shouldn't package it.
This app contains a number of hard-coded test scenarios for visual inspection.
It uses the real niri layout and rendering code, but with mock windows instead of Wayland clients.
The idea is to go through the test scenarios and check that everything *looks* right.
## Running
You will need recent GTK and libadwaita.
Then, `cargo run`.
+3
View File
@@ -0,0 +1,3 @@
.anim-control-bar {
padding: 12px;
}
@@ -0,0 +1,76 @@
use std::f32::consts::{FRAC_PI_2, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::render_helpers::gradient::GradientRenderElement;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Rectangle, Scale, Size};
use super::TestCase;
pub struct GradientAngle {
angle: f32,
prev_time: Duration,
}
impl GradientAngle {
pub fn new(_size: Size<i32, Logical>) -> Self {
Self {
angle: 0.,
prev_time: Duration::ZERO,
}
}
}
impl TestCase for GradientAngle {
fn are_animations_ongoing(&self) -> bool {
true
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.angle += delta.as_secs_f32() * PI;
if self.angle >= PI * 2. {
self.angle -= PI * 2.
}
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size);
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
self.angle - FRAC_PI_2,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
@@ -0,0 +1,116 @@
use std::f32::consts::{FRAC_PI_4, PI};
use std::sync::atomic::Ordering;
use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::gradient::GradientRenderElement;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use super::TestCase;
pub struct GradientArea {
progress: f32,
border: FocusRing,
prev_time: Duration,
}
impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let mut border = FocusRing::new(niri_config::FocusRing {
off: false,
width: 1,
active_color: Color::new(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
inactive_gradient: None,
});
border.set_active(true);
Self {
progress: 0.,
border,
prev_time: Duration::ZERO,
}
}
}
impl TestCase for GradientArea {
fn are_animations_ongoing(&self) -> bool {
true
}
fn advance_animations(&mut self, current_time: Duration) {
let mut delta = if self.prev_time.is_zero() {
Duration::ZERO
} else {
current_time.saturating_sub(self.prev_time)
};
self.prev_time = current_time;
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
if slowdown == 0. {
delta = Duration::ZERO
} else {
delta = delta.div_f64(slowdown);
}
self.progress += delta.as_secs_f32() * PI;
if self.progress >= PI * 2. {
self.progress -= PI * 2.
}
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let mut rv = Vec::new();
let f = (self.progress.sin() + 1.) / 2.;
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size);
let g_size = Size::from((
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
));
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
let g_area = Rectangle::from_loc_and_size(g_loc, g_size);
self.border.update(g_size, true);
rv.extend(
self.border
.render(
renderer,
Point::from(g_loc),
Scale::from(1.),
size.to_logical(1),
)
.map(|elem| Box::new(elem) as _),
);
rv.extend(
GradientRenderElement::new(
renderer,
Scale::from(1.),
area,
g_area,
[1., 0., 0., 1.],
[0., 1., 0., 1.],
FRAC_PI_4,
)
.into_iter()
.map(|elem| Box::new(elem) as _),
);
rv
}
}
+231
View File
@@ -0,0 +1,231 @@
use std::collections::HashMap;
use std::time::Duration;
use niri::layout::workspace::ColumnWidth;
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;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::utils::{Logical, Physical, Size};
use super::TestCase;
use crate::test_window::TestWindow;
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
pub struct Layout {
output: Output,
windows: Vec<TestWindow>,
layout: niri::layout::Layout<TestWindow>,
start_time: Duration,
steps: HashMap<Duration, DynStepFn>,
}
impl Layout {
pub fn new(size: Size<i32, Logical>) -> Self {
let output = Output::new(
String::new(),
PhysicalProperties {
size: Size::from((size.w, size.h)),
subpixel: Subpixel::Unknown,
make: String::new(),
model: String::new(),
},
);
let mode = Some(Mode {
size: size.to_physical(1),
refresh: 60000,
});
output.change_current_state(mode, None, None, None);
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: 4,
active_color: Color::new(255, 163, 72, 255),
inactive_color: Color::new(50, 50, 50, 255),
active_gradient: None,
inactive_gradient: None,
},
..Default::default()
};
let mut layout = niri::layout::Layout::with_options(options);
layout.add_output(output.clone());
Self {
output,
windows: Vec::new(),
layout,
start_time: get_monotonic_time(),
steps: HashMap::new(),
}
}
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
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(&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.id());
});
rv
}
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
for delay in [100, 200, 300] {
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.id());
});
}
rv
}
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
for delay in [100, 200, 300] {
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.id());
});
}
rv
}
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
rv.add_step(500, |l| {
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.id());
});
rv
}
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
let mut rv = Self::new(size);
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
rv.add_step(500, |l| {
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.id());
});
rv
}
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.id());
}
self.windows.push(window);
}
fn add_window_right_of(
&mut self,
right_of: &TestWindow,
window: TestWindow,
width: Option<ColumnWidth>,
) {
self.layout
.add_window_right_of(right_of.id(), window.clone(), width, false);
if window.communicate() {
self.layout.update_window(window.id());
}
self.windows.push(window);
}
fn add_step(&mut self, delay_ms: u64, f: impl FnOnce(&mut Self) + 'static) {
self.steps
.insert(Duration::from_millis(delay_ms), Box::new(f) as _);
}
}
impl TestCase for Layout {
fn resize(&mut self, width: i32, height: i32) {
let mode = Some(Mode {
size: Size::from((width, height)),
refresh: 60000,
});
self.output.change_current_state(mode, None, None, None);
layer_map_for_output(&self.output).arrange();
self.layout.update_output_size(&self.output);
for win in &self.windows {
if win.communicate() {
self.layout.update_window(win.id());
}
}
}
fn are_animations_ongoing(&self) -> bool {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.are_animations_ongoing()
|| !self.steps.is_empty()
}
fn advance_animations(&mut self, mut current_time: Duration) {
let run = self
.steps
.keys()
.copied()
.filter(|delay| self.start_time + *delay <= current_time)
.collect::<Vec<_>>();
for key in &run {
let f = self.steps.remove(key).unwrap();
f(self);
}
if !run.is_empty() {
current_time = get_monotonic_time();
}
self.layout.advance_animations(current_time);
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
_size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_elements(renderer, RenderTarget::Output)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+24
View File
@@ -0,0 +1,24 @@
use std::time::Duration;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Size};
pub mod gradient_angle;
pub mod gradient_area;
pub mod layout;
pub mod tile;
pub mod window;
pub trait TestCase {
fn resize(&mut self, _width: i32, _height: i32) {}
fn are_animations_ongoing(&self) -> bool {
false
}
fn advance_animations(&mut self, _current_time: Duration) {}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>>;
}
+119
View File
@@ -0,0 +1,119 @@
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;
use smithay::utils::{Logical, Physical, Point, Scale, Size};
use super::TestCase;
use crate::test_window::TestWindow;
pub struct Tile {
window: TestWindow,
tile: niri::layout::tile::Tile<TestWindow>,
}
impl Tile {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size);
rv.window.communicate();
rv
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size);
rv.window.communicate();
rv
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size);
rv.window.communicate();
rv
}
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::freeform(size);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::fixed_size(size);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
let mut rv = Self::fixed_size_with_csd_shadow(size);
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
rv.tile.start_open_animation();
rv
}
pub fn with_window(window: TestWindow) -> Self {
let options = Options {
focus_ring: niri_config::FocusRing {
off: true,
..Default::default()
},
border: niri_config::Border {
off: false,
width: 32,
active_color: Color::new(255, 163, 72, 255),
..Default::default()
},
..Default::default()
};
let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options));
Self { window, tile }
}
}
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
self.tile.request_tile_size(Size::from((width, height)));
self.window.communicate();
}
fn are_animations_ongoing(&self) -> bool {
self.tile.are_animations_ongoing()
}
fn advance_animations(&mut self, current_time: Duration) {
self.tile.advance_animations(current_time, true);
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let tile_size = self.tile.tile_size().to_physical(1);
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
self.tile
.render(
renderer,
location,
Scale::from(1.),
size.to_logical(1),
true,
RenderTarget::Output,
)
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+64
View File
@@ -0,0 +1,64 @@
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};
use super::TestCase;
use crate::test_window::TestWindow;
pub struct Window {
window: TestWindow,
}
impl Window {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
window.request_size(size);
window.communicate();
Self { window }
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.request_size(size);
window.communicate();
Self { window }
}
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(size);
window.communicate();
Self { window }
}
}
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window.request_size(Size::from((width, height)));
self.window.communicate();
}
fn render(
&mut self,
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let win_size = self.window.size().to_physical(1);
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
self.window
.render(
renderer,
location,
Scale::from(1.),
1.,
RenderTarget::Output,
)
.into_iter()
.map(|elem| Box::new(elem) as _)
.collect()
}
}
+170
View File
@@ -0,0 +1,170 @@
#[macro_use]
extern crate tracing;
use std::env;
use std::sync::atomic::Ordering;
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
use cases::tile::Tile;
use cases::window::Window;
use gtk::prelude::{
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
};
use gtk::{gdk, gio, glib};
use niri::animation::ANIMATION_SLOWDOWN;
use smithay::utils::{Logical, Size};
use smithay_view::SmithayView;
use tracing_subscriber::EnvFilter;
use crate::cases::gradient_angle::GradientAngle;
use crate::cases::gradient_area::GradientArea;
use crate::cases::layout::Layout;
use crate::cases::TestCase;
mod cases;
mod smithay_view;
mod test_window;
fn main() -> glib::ExitCode {
let directives =
env::var("RUST_LOG").unwrap_or_else(|_| "niri-visual-tests=debug,niri=debug".to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
tracing_subscriber::fmt()
.compact()
.with_env_filter(env_filter)
.init();
let app = adw::Application::new(None::<&str>, gio::ApplicationFlags::NON_UNIQUE);
app.connect_startup(on_startup);
app.connect_activate(build_ui);
app.run()
}
fn on_startup(_app: &adw::Application) {
// Load our CSS.
let provider = gtk::CssProvider::new();
provider.load_from_string(include_str!("../resources/style.css"));
if let Some(display) = gdk::Display::default() {
gtk::style_context_add_provider_for_display(
&display,
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
fn build_ui(app: &adw::Application) {
let stack = gtk::Stack::new();
struct S {
stack: gtk::Stack,
}
impl S {
fn add<T: TestCase + 'static>(
&self,
make: impl Fn(Size<i32, Logical>) -> T + 'static,
title: &str,
) {
let view = SmithayView::new(make);
self.stack.add_titled(&view, None, title);
}
}
let s = S {
stack: stack.clone(),
};
s.add(Window::freeform, "Freeform Window");
s.add(Window::fixed_size, "Fixed Size Window");
s.add(
Window::fixed_size_with_csd_shadow,
"Fixed Size Window - CSD Shadow",
);
s.add(Tile::freeform, "Freeform Tile");
s.add(Tile::fixed_size, "Fixed Size Tile");
s.add(
Tile::fixed_size_with_csd_shadow,
"Fixed Size Tile - CSD Shadow",
);
s.add(Tile::freeform_open, "Freeform Tile - Open");
s.add(Tile::fixed_size_open, "Fixed Size Tile - Open");
s.add(
Tile::fixed_size_with_csd_shadow_open,
"Fixed Size Tile - CSD Shadow - Open",
);
s.add(Layout::open_in_between, "Layout - Open In-Between");
s.add(
Layout::open_multiple_quickly,
"Layout - Open Multiple Quickly",
);
s.add(
Layout::open_multiple_quickly_big,
"Layout - Open Multiple Quickly - Big",
);
s.add(Layout::open_to_the_left, "Layout - Open To The Left");
s.add(
Layout::open_to_the_left_big,
"Layout - Open To The Left - Big",
);
s.add(GradientAngle::new, "Gradient - Angle");
s.add(GradientArea::new, "Gradient - Area");
let content_headerbar = adw::HeaderBar::new();
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
anim_adjustment
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
anim_scale.set_hexpand(true);
let anim_control_bar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
anim_control_bar.add_css_class("anim-control-bar");
anim_control_bar.append(&gtk::Label::new(Some("Slowdown")));
anim_control_bar.append(&anim_scale);
let content_view = adw::ToolbarView::new();
content_view.set_top_bar_style(adw::ToolbarStyle::RaisedBorder);
content_view.set_bottom_bar_style(adw::ToolbarStyle::RaisedBorder);
content_view.add_top_bar(&content_headerbar);
content_view.add_bottom_bar(&anim_control_bar);
content_view.set_content(Some(&stack));
let content = adw::NavigationPage::new(
&content_view,
stack
.page(&stack.visible_child().unwrap())
.title()
.as_deref()
.unwrap(),
);
let sidebar_header = adw::HeaderBar::new();
let stack_sidebar = gtk::StackSidebar::new();
stack_sidebar.set_stack(&stack);
let sidebar_view = adw::ToolbarView::new();
sidebar_view.add_top_bar(&sidebar_header);
sidebar_view.set_content(Some(&stack_sidebar));
let sidebar = adw::NavigationPage::new(&sidebar_view, "Tests");
let split_view = adw::NavigationSplitView::new();
split_view.set_content(Some(&content));
split_view.set_sidebar(Some(&sidebar));
stack.connect_visible_child_notify(move |stack| {
content.set_title(
stack
.visible_child()
.and_then(|c| stack.page(&c).title())
.as_deref()
.unwrap_or_default(),
)
});
let window = adw::ApplicationWindow::new(app);
window.set_title(Some("niri visual tests"));
window.set_content(Some(&split_view));
window.present();
}
+250
View File
@@ -0,0 +1,250 @@
use gtk::glib;
use gtk::subclass::prelude::*;
use smithay::utils::{Logical, Size};
use crate::cases::TestCase;
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
use std::ptr::null;
use anyhow::{ensure, Context};
use gtk::gdk;
use gtk::prelude::*;
use niri::render_helpers::shaders;
use niri::utils::get_monotonic_time;
use smithay::backend::egl::ffi::egl;
use smithay::backend::egl::EGLContext;
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::{Frame, Renderer, Unbind};
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
#[derive(Default)]
pub struct SmithayView {
gl_area: gtk::GLArea,
size: Cell<(i32, i32)>,
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
pub make_test_case: OnceCell<DynMakeTestCase>,
test_case: RefCell<Option<Box<dyn TestCase>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SmithayView {
const NAME: &'static str = "NiriSmithayView";
type Type = super::SmithayView;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk::BinLayout>();
}
}
impl ObjectImpl for SmithayView {
fn constructed(&self) {
let obj = self.obj();
self.parent_constructed();
self.gl_area.set_allowed_apis(gdk::GLAPI::GLES);
self.gl_area.set_parent(&*obj);
self.gl_area.connect_resize({
let imp = self.downgrade();
move |_, width, height| {
if let Some(imp) = imp.upgrade() {
imp.resize(width, height);
}
}
});
self.gl_area.connect_render({
let imp = self.downgrade();
move |_, gl_context| {
if let Some(imp) = imp.upgrade() {
if let Err(err) = imp.render(gl_context) {
warn!("error rendering: {err:?}");
}
}
glib::Propagation::Stop
}
});
obj.add_tick_callback(|obj, _frame_clock| {
let imp = obj.imp();
if let Some(case) = &mut *imp.test_case.borrow_mut() {
if case.are_animations_ongoing() {
imp.gl_area.queue_draw();
}
}
glib::ControlFlow::Continue
});
}
fn dispose(&self) {
self.gl_area.unparent();
}
}
impl WidgetImpl for SmithayView {
fn unmap(&self) {
self.test_case.replace(None);
self.parent_unmap();
}
fn unrealize(&self) {
self.renderer.replace(None);
self.parent_unrealize();
}
}
impl SmithayView {
fn resize(&self, width: i32, height: i32) {
self.size.set((width, height));
if let Some(case) = &mut *self.test_case.borrow_mut() {
case.resize(width, height);
}
}
fn render(&self, _gl_context: &gdk::GLContext) -> anyhow::Result<()> {
// Set up the Smithay renderer.
let mut renderer = self.renderer.borrow_mut();
let renderer = renderer.get_or_insert_with(|| {
unsafe { create_renderer() }
.map_err(|err| warn!("error creating a Smithay renderer: {err:?}"))
});
let Ok(renderer) = renderer else {
return Ok(());
};
let size = self.size.get();
// Create the test case if missing.
let mut case = self.test_case.borrow_mut();
let case = case.get_or_insert_with(|| {
let make = self.make_test_case.get().unwrap();
make(Size::from(size))
});
case.advance_animations(get_monotonic_time());
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
let elements = unsafe {
with_framebuffer_save_restore(renderer, |renderer| {
case.render(renderer, Size::from(size))
})
}?;
let mut frame = renderer
.render(rect.size, Transform::Normal)
.context("error creating frame")?;
frame
.clear([0.3, 0.3, 0.3, 1.], &[rect])
.context("error clearing")?;
for element in elements.iter().rev() {
let src = element.src();
let dst = element.geometry(Scale::from(1.));
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
Ok(())
}
}
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
.context("error loading EGL symbols in Smithay")?;
let egl_display = egl::GetCurrentDisplay();
ensure!(egl_display != egl::NO_DISPLAY, "no current EGL display");
let egl_context = egl::GetCurrentContext();
ensure!(egl_context != egl::NO_CONTEXT, "no current EGL context");
// There's no config ID on the EGL context and there's no current EGL surface, but we don't
// really use it anyway so just get some random one.
let mut egl_config_id = null();
let mut num_configs = 0;
let res = egl::GetConfigs(egl_display, &mut egl_config_id, 1, &mut num_configs);
ensure!(res == egl::TRUE, "error choosing EGL config");
ensure!(num_configs != 0, "no EGL config");
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
.context("error creating EGL context")?;
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
.context("error getting supported renderer capabilities")?
.into_iter()
.filter(|c| *c != Capability::ColorTransformations);
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
.context("error creating GlesRenderer")?;
shaders::init(&mut renderer);
Ok(renderer)
}
unsafe fn with_framebuffer_save_restore<T>(
renderer: &mut GlesRenderer,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> anyhow::Result<T> {
let mut framebuffer = 0;
renderer
.with_context(|gl| unsafe {
gl.GetIntegerv(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
&mut framebuffer,
);
})
.context("error running closure in GL context")?;
ensure!(framebuffer != 0, "error getting the framebuffer");
let rv = f(renderer);
renderer.unbind().context("error unbinding")?;
renderer
.with_context(|gl| unsafe {
gl.BindFramebuffer(
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
framebuffer as u32,
);
})
.context("error running closure in GL context")?;
Ok(rv)
}
}
glib::wrapper! {
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
@extends gtk::Widget;
}
impl SmithayView {
pub fn new<T: TestCase + 'static>(
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
) -> Self {
let obj: Self = glib::Object::builder().build();
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
let make_test_case = Box::new(make) as _;
let _ = obj.imp().make_test_case.set(make_test_case);
obj
}
}
+229
View File
@@ -0,0 +1,229 @@
use std::cell::RefCell;
use std::cmp::{max, min};
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;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Scale, Size, Transform};
#[derive(Debug)]
struct TestWindowInner {
size: Size<i32, Logical>,
requested_size: Option<Size<i32, Logical>>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
buffer: SolidColorBuffer,
pending_fullscreen: bool,
csd_shadow_width: i32,
csd_shadow_buffer: SolidColorBuffer,
}
#[derive(Debug, Clone)]
pub struct TestWindow {
id: usize,
inner: Rc<RefCell<TestWindowInner>>,
}
impl TestWindow {
pub fn freeform(id: usize) -> Self {
let size = Size::from((100, 200));
let min_size = Size::from((0, 0));
let max_size = Size::from((0, 0));
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
Self {
id,
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 {
let rv = Self::freeform(id);
rv.set_min_size((200, 400).into());
rv.set_max_size((200, 400).into());
rv.set_color([0.88, 0.11, 0.14, 1.]);
rv.communicate();
rv
}
pub fn set_min_size(&self, size: Size<i32, Logical>) {
self.inner.borrow_mut().min_size = size;
}
pub fn set_max_size(&self, size: Size<i32, Logical>) {
self.inner.borrow_mut().max_size = size;
}
pub fn set_color(&self, color: [f32; 4]) {
self.inner.borrow_mut().buffer.set_color(color);
}
pub fn set_csd_shadow_width(&self, width: i32) {
self.inner.borrow_mut().csd_shadow_width = width;
}
pub fn communicate(&self) -> bool {
let mut rv = false;
let mut inner = self.inner.borrow_mut();
let mut new_size = inner.size;
if let Some(size) = inner.requested_size.take() {
assert!(size.w >= 0);
assert!(size.h >= 0);
if size.w != 0 {
new_size.w = size.w;
}
if size.h != 0 {
new_size.h = size.h;
}
}
if inner.max_size.w > 0 {
new_size.w = min(new_size.w, inner.max_size.w);
}
if inner.max_size.h > 0 {
new_size.h = min(new_size.h, inner.max_size.h);
}
if inner.min_size.w > 0 {
new_size.w = max(new_size.w, inner.min_size.w);
}
if inner.min_size.h > 0 {
new_size.h = max(new_size.h, inner.min_size.h);
}
if inner.size != new_size {
inner.size = new_size;
inner.buffer.resize(new_size);
rv = true;
}
let mut csd_shadow_size = new_size;
csd_shadow_size.w += inner.csd_shadow_width * 2;
csd_shadow_size.h += inner.csd_shadow_width * 2;
inner.csd_shadow_buffer.resize(csd_shadow_size);
rv
}
}
impl LayoutElement for TestWindow {
type Id = usize;
fn id(&self) -> &Self::Id {
&self.id
}
fn size(&self) -> Size<i32, Logical> {
self.inner.borrow().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
(0, 0).into()
}
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
false
}
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
alpha: f32,
_target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
let inner = self.inner.borrow();
vec![
SolidColorRenderElement::from_buffer(
&inner.buffer,
location.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
SolidColorRenderElement::from_buffer(
&inner.csd_shadow_buffer,
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
.to_physical_precise_round(scale),
scale,
alpha,
Kind::Unspecified,
)
.into(),
]
}
fn request_size(&self, size: Size<i32, Logical>) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.inner.borrow_mut().pending_fullscreen = true;
}
fn min_size(&self) -> Size<i32, Logical> {
self.inner.borrow().min_size
}
fn max_size(&self) -> Size<i32, Logical> {
self.inner.borrow().max_size
}
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
false
}
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
fn has_ssd(&self) -> bool {
false
}
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) {}
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.inner.borrow().pending_fullscreen
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
}
+304 -106
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,114 +16,184 @@ 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.
// Omitting settings disables them, or leaves them at their default values.
touchpad {
tap
// dwt
// dwtp
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
}
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"
mouse {
// 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
// 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 with wayland-info(1).
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Remember to uncommend the node by removing "/-"!
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
// Uncomment this line to disable this output.
// off
// Resolution and, optionally, refresh rate of the output.
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
// If the refresh rate is omitted, niri will pick the highest refresh rate
// for the resolution.
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
// All valid modes are listed in niri's debug output when an output is connected.
mode "1920x1080@144"
// 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.
//
// 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 {
// Uncomment this line to disable the focus ring.
// off
// How many logical pixels the ring extends out from the windows.
width 4
// 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.
// Color of the ring on the active monitor.
active-color "#7fc8ff"
// Color of the ring on inactive monitors.
inactive-color "#505050"
// 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,
// defaulting to 180 (top-to-bottom gradient).
// You can use any CSS linear-gradient tool on the web to set these up.
//
// active-gradient from="#80c8ff" to="#bbddff" angle=45
// You can also color the gradient relative to the entire view
// of the workspace, rather than relative to just the window itself.
// To do that, set relative-to="workspace-view".
//
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can also add a border. It's similar to the focus ring, but always visible.
border {
// The settings are the same as for the focus ring.
// If you enable the border, you probably want to disable the focus ring.
off
width 4
active-color "#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 shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
// layer-shell panels and regular gaps.
struts {
// left 64
// right 64
// top 64
// bottom 64
}
}
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// spawn-at-startup "alacritty" "-e" "fish"
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
// off
// How many logical pixels the ring extends out from the windows.
width 4
// Color of the ring on the active monitor: red, green, blue, alpha.
active-color 127 200 255 255
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
}
cursor {
// Change the theme and size of the cursor as well as set the
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
// xcursor-theme "default"
// xcursor-size 24
}
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// If the client will specifically ask for CSD, the request will be honored.
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
// prefer-no-csd
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.333
proportion 0.5
proportion 0.667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// Set gaps around windows in logical pixels.
gaps 16
// You can change the path where screenshots are saved.
// 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.
@@ -127,6 +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
// 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
}
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// 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.
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
@@ -134,11 +246,21 @@ binds {
//
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
// when running as a winit window.
//
// Most actions that you can bind here can also be invoked programmatically with
// `niri msg action do-something`.
// Mod-Shift-/, which is usually the same as Mod-?,
// shows a list of important hotkeys.
Mod+Shift+Slash { show-hotkey-overlay; }
// Suggested binds for running programs: terminal, app launcher, screen locker.
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Mod+Alt+L { spawn "swaylock"; }
Super+Alt+L { spawn "swaylock"; }
// You can also use a shell:
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
// Example volume keys mappings for PipeWire & WirePlumber.
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
@@ -146,56 +268,120 @@ binds {
Mod+Q { close-window; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+J { move-window-down; }
Mod+Ctrl+K { move-window-up; }
Mod+Ctrl+L { move-column-right; }
Mod+Ctrl+Left { move-column-left; }
Mod+Ctrl+Down { move-window-down; }
Mod+Ctrl+Up { move-window-up; }
Mod+Ctrl+Right { move-column-right; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+J { move-window-down; }
Mod+Ctrl+K { move-window-up; }
Mod+Ctrl+L { move-column-right; }
// Alternative commands that move across workspaces when reaching
// the first or last window in a column.
// Mod+J { focus-window-or-workspace-down; }
// Mod+K { focus-window-or-workspace-up; }
// Mod+Ctrl+J { move-window-down-or-to-workspace-down; }
// Mod+Ctrl+K { move-window-up-or-to-workspace-up; }
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
Mod+Shift+H { focus-monitor-left; }
Mod+Shift+J { focus-monitor-down; }
Mod+Shift+K { focus-monitor-up; }
Mod+Shift+L { focus-monitor-right; }
Mod+Shift+Left { focus-monitor-left; }
Mod+Shift+Down { focus-monitor-down; }
Mod+Shift+Up { focus-monitor-up; }
Mod+Shift+Right { focus-monitor-right; }
Mod+Shift+H { focus-monitor-left; }
Mod+Shift+J { focus-monitor-down; }
Mod+Shift+K { focus-monitor-up; }
Mod+Shift+L { focus-monitor-right; }
Mod+Shift+Ctrl+H { move-window-to-monitor-left; }
Mod+Shift+Ctrl+J { move-window-to-monitor-down; }
Mod+Shift+Ctrl+K { move-window-to-monitor-up; }
Mod+Shift+Ctrl+L { move-window-to-monitor-right; }
Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-window-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-window-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-window-to-monitor-right; }
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// Alternatively, there are commands to move just a single window:
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
// ...
// And you can also move a whole workspace to another monitor:
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
// ...
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+Ctrl+U { move-window-to-workspace-down; }
Mod+Ctrl+I { move-window-to-workspace-up; }
Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
Mod+Ctrl+Page_Up { move-window-to-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
// ...
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// 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
// the current workspace count will instead refer to the bottommost
// (empty) workspace.
//
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
// will all refer to the 3rd workspace.
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
@@ -205,19 +391,29 @@ binds {
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
Mod+Ctrl+1 { move-window-to-workspace 1; }
Mod+Ctrl+2 { move-window-to-workspace 2; }
Mod+Ctrl+3 { move-window-to-workspace 3; }
Mod+Ctrl+4 { move-window-to-workspace 4; }
Mod+Ctrl+5 { move-window-to-workspace 5; }
Mod+Ctrl+6 { move-window-to-workspace 6; }
Mod+Ctrl+7 { move-window-to-workspace 7; }
Mod+Ctrl+8 { move-window-to-workspace 8; }
Mod+Ctrl+9 { move-window-to-workspace 9; }
Mod+Ctrl+1 { move-column-to-workspace 1; }
Mod+Ctrl+2 { move-column-to-workspace 2; }
Mod+Ctrl+3 { move-column-to-workspace 3; }
Mod+Ctrl+4 { move-column-to-workspace 4; }
Mod+Ctrl+5 { move-column-to-workspace 5; }
Mod+Ctrl+6 { move-column-to-workspace 6; }
Mod+Ctrl+7 { move-column-to-workspace 7; }
Mod+Ctrl+8 { move-column-to-workspace 8; }
Mod+Ctrl+9 { move-column-to-workspace 9; }
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+1 { move-window-to-workspace 1; }
// 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; }
// There are also commands that consume or expel a single window to the side.
// Mod+BracketLeft { consume-or-expel-window-left; }
// Mod+BracketRight { consume-or-expel-window-right; }
Mod+R { switch-preset-column-width; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
@@ -250,8 +446,10 @@ binds {
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Mod+Shift+P { power-off-monitors; }
Mod+Shift+Ctrl+T { toggle-debug-tint; }
// 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; }
}
+1 -7
View File
@@ -20,12 +20,6 @@ fi
# Reset failed state of all user units.
systemctl --user reset-failed
# Set the current desktop for xdg-desktop-portal.
export XDG_CURRENT_DESKTOP=niri
# Ensure the session type is set to Wayland for xdg-autostart apps.
export XDG_SESSION_TYPE=wayland
# Import the login manager environment.
systemctl --user import-environment
@@ -44,4 +38,4 @@ systemctl --user --wait start niri.service
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
# Unset environment that we've set.
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
+2 -1
View File
@@ -9,5 +9,6 @@ Wants=xdg-desktop-autostart.target
Before=xdg-desktop-autostart.target
[Service]
Slice=session.slice
Type=notify
ExecStart=/usr/bin/niri
ExecStart=/usr/bin/niri --session
-53
View File
@@ -1,53 +0,0 @@
use std::time::Duration;
use keyframe::functions::EaseOutCubic;
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)]
pub struct Animation {
from: f64,
to: f64,
duration: Duration,
start_time: Duration,
current_time: Duration,
}
impl Animation {
pub fn new(from: f64, to: f64, over: Duration) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
Self {
from,
to,
duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)),
start_time: now,
current_time: now,
}
}
pub fn set_current_time(&mut self, time: Duration) {
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
}
pub fn value(&self) -> f64 {
let passed = (self.current_time - self.start_time).as_secs_f64();
let total = self.duration.as_secs_f64();
let x = (passed / total).clamp(0., 1.);
EaseOutCubic.y(x) * (self.to - self.from) + self.from
}
pub fn to(&self) -> f64 {
self.to
}
}
+282
View File
@@ -0,0 +1,282 @@
use std::time::Duration;
use keyframe::functions::EaseOutCubic;
use keyframe::EasingFunction;
use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time;
mod spring;
pub use spring::{Spring, SpringParams};
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)]
pub struct Animation {
from: f64,
to: f64,
duration: Duration,
start_time: Duration,
current_time: Duration,
kind: Kind,
}
#[derive(Debug, Clone, Copy)]
enum Kind {
Easing {
curve: Curve,
},
Spring(Spring),
Deceleration {
initial_velocity: f64,
deceleration_rate: f64,
},
}
#[derive(Debug, Clone, Copy)]
pub enum Curve {
EaseOutCubic,
EaseOutExpo,
}
impl Animation {
pub fn new(
from: f64,
to: f64,
initial_velocity: f64,
config: niri_config::Animation,
default: niri_config::Animation,
) -> Self {
if config.off {
return Self::ease(from, to, 0, Curve::EaseOutCubic);
}
// Resolve defaults.
let (kind, easing_defaults) = match (config.kind, default.kind) {
// Configured spring.
(configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None),
// Configured nothing, defaults spring.
(
niri_config::AnimationKind::Easing(easing),
defaults @ niri_config::AnimationKind::Spring(_),
) if easing == niri_config::EasingParams::unfilled() => (defaults, None),
// Configured easing or nothing, defaults easing.
(
configured @ niri_config::AnimationKind::Easing(_),
niri_config::AnimationKind::Easing(defaults),
) => (configured, Some(defaults)),
// Configured easing, defaults spring.
(
configured @ niri_config::AnimationKind::Easing(_),
niri_config::AnimationKind::Spring(_),
) => (configured, None),
};
match kind {
niri_config::AnimationKind::Spring(p) => {
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
let spring = Spring {
from,
to,
initial_velocity,
params,
};
Self::spring(spring)
}
niri_config::AnimationKind::Easing(p) => {
let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default());
let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap();
let curve = Curve::from(p.curve.or(defaults.curve).unwrap());
Self::ease(from, to, u64::from(duration_ms), curve)
}
}
}
pub fn ease(from: f64, to: f64, duration_ms: u64, curve: Curve) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = Duration::from_millis(duration_ms);
let kind = Kind::Easing { curve };
Self {
from,
to,
duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn spring(spring: Spring) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration = spring.duration();
let kind = Kind::Spring(spring);
Self {
from: spring.from,
to: spring.to,
duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn decelerate(
from: f64,
initial_velocity: f64,
deceleration_rate: f64,
threshold: f64,
) -> Self {
// FIXME: ideally we shouldn't use current time here because animations started within the
// same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time();
let duration_s = if initial_velocity == 0. {
0.
} else {
let coeff = 1000. * deceleration_rate.ln();
(-coeff * threshold / initial_velocity.abs()).ln() / coeff
};
let duration = Duration::from_secs_f64(duration_s);
let to = from - initial_velocity / (1000. * deceleration_rate.ln());
let kind = Kind::Deceleration {
initial_velocity,
deceleration_rate,
};
Self {
from,
to,
duration,
start_time: now,
current_time: now,
kind,
}
}
pub fn set_current_time(&mut self, time: Duration) {
if self.duration.is_zero() {
self.current_time = time;
return;
}
let end_time = self.start_time + self.duration;
if end_time <= self.current_time {
return;
}
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
if slowdown <= f64::EPSILON {
// Zero slowdown will cause the animation to end right away.
self.current_time = end_time;
return;
}
// We can't change current_time (since the incoming time values are always real-time), so
// apply the slowdown by shifting the start time to compensate.
if self.current_time <= time {
let delta = time - self.current_time;
let max_delta = end_time - self.current_time;
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
if slowdown <= min_slowdown {
// Our slowdown value will cause the animation to end right away.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time -= adjusted_delta - delta;
} else {
self.start_time += delta - adjusted_delta;
}
} else {
let delta = self.current_time - time;
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
if slowdown <= min_slowdown {
// Current time was about to jump to before the animation had started; let's just
// cancel the animation in this case.
self.current_time = end_time;
return;
}
let adjusted_delta = delta.div_f64(slowdown);
if adjusted_delta >= delta {
self.start_time += adjusted_delta - delta;
} else {
self.start_time -= delta - adjusted_delta;
}
}
self.current_time = time;
}
pub fn is_done(&self) -> bool {
self.current_time >= self.start_time + self.duration
}
pub fn value(&self) -> f64 {
if self.is_done() {
return self.to;
}
let passed = self.current_time - self.start_time;
match self.kind {
Kind::Easing { curve } => {
let passed = passed.as_secs_f64();
let total = self.duration.as_secs_f64();
let x = (passed / total).clamp(0., 1.);
curve.y(x) * (self.to - self.from) + self.from
}
Kind::Spring(spring) => spring.value_at(passed),
Kind::Deceleration {
initial_velocity,
deceleration_rate,
} => {
let passed = passed.as_secs_f64();
let coeff = 1000. * deceleration_rate.ln();
self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity
}
}
}
pub fn to(&self) -> f64 {
self.to
}
#[cfg(test)]
pub fn from(&self) -> f64 {
self.from
}
}
impl Curve {
pub fn y(self, x: f64) -> f64 {
match self {
Curve::EaseOutCubic => EaseOutCubic.y(x),
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
}
}
}
impl From<niri_config::AnimationCurve> for Curve {
fn from(value: niri_config::AnimationCurve) -> Self {
match value {
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
}
}
}
+137
View File
@@ -0,0 +1,137 @@
use std::time::Duration;
#[derive(Debug, Clone, Copy)]
pub struct SpringParams {
pub damping: f64,
pub mass: f64,
pub stiffness: f64,
pub epsilon: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct Spring {
pub from: f64,
pub to: f64,
pub initial_velocity: f64,
pub params: SpringParams,
}
impl SpringParams {
pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self {
let damping_ratio = damping_ratio.max(0.);
let stiffness = stiffness.max(0.);
let epsilon = epsilon.max(0.);
let mass = 1.;
let critical_damping = 2. * (mass * stiffness).sqrt();
let damping = damping_ratio * critical_damping;
Self {
damping,
mass,
stiffness,
epsilon,
}
}
}
impl Spring {
pub fn value_at(&self, t: Duration) -> f64 {
self.oscillate(t.as_secs_f64())
}
// Based on libadwaita (LGPL-2.1-or-later):
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c,
// which itself is based on (MIT):
// https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m
/// Computes and returns the duration until the spring is at rest.
pub fn duration(&self) -> Duration {
const DELTA: f64 = 0.001;
let beta = self.params.damping / (2. * self.params.mass);
if beta.abs() <= f64::EPSILON || beta < 0. {
return Duration::MAX;
}
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
// As first ansatz for the overdamped solution,
// and general estimation for the oscillating ones
// we take the value of the envelope when it's < epsilon.
let mut x0 = -self.params.epsilon.ln() / beta;
// f64::EPSILON is too small for this specific comparison, so we use
// f32::EPSILON even though it's doubles.
if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 {
return Duration::from_secs_f64(x0);
}
// Since the overdamped solution decays way slower than the envelope
// we need to use the value of the oscillation itself.
// Newton's root finding method is a good candidate in this particular case:
// https://en.wikipedia.org/wiki/Newton%27s_method
let mut y0 = self.oscillate(x0);
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
let mut x1 = (self.to - y0 + m * x0) / m;
let mut y1 = self.oscillate(x1);
let mut i = 0;
while (self.to - y1).abs() > self.params.epsilon {
if i > 1000 {
return Duration::ZERO;
}
x0 = x1;
y0 = y1;
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
x1 = (self.to - y0 + m * x0) / m;
y1 = self.oscillate(x1);
i += 1;
}
Duration::from_secs_f64(x1)
}
/// Returns the spring position at a given time in seconds.
fn oscillate(&self, t: f64) -> f64 {
let b = self.params.damping;
let m = self.params.mass;
let k = self.params.stiffness;
let v0 = self.initial_velocity;
let beta = b / (2. * m);
let omega0 = (k / m).sqrt();
let x0 = self.from - self.to;
let envelope = (-beta * t).exp();
// Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x)
// for the differential equation m*ẍ+b*ẋ+kx = 0
// f64::EPSILON is too small for this specific comparison, so we use
// f32::EPSILON even though it's doubles.
if (beta - omega0).abs() <= f64::from(f32::EPSILON) {
// Critically damped.
self.to + envelope * (x0 + (beta * x0 + v0) * t)
} else if beta < omega0 {
// Underdamped.
let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt();
self.to
+ envelope
* (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin())
} else {
// Overdamped.
let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt();
self.to
+ envelope
* (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh())
}
}
}
+38 -20
View File
@@ -2,12 +2,13 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use crate::input::CompositorMod;
use crate::niri::OutputRenderElements;
use crate::Niri;
use crate::niri::Niri;
pub mod tty;
pub use tty::Tty;
@@ -26,10 +27,12 @@ pub enum RenderResult {
Submitted,
/// Rendering succeeded, but there was no damage.
NoDamage,
/// An error has occurred, the frame was not submitted.
Error,
/// The frame was not rendered and submitted, due to an error or otherwise.
Skipped,
}
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
impl Backend {
pub fn init(&mut self, niri: &mut Niri) {
match self {
@@ -45,10 +48,13 @@ impl Backend {
}
}
pub fn renderer(&mut self) -> Option<&mut GlesRenderer> {
pub fn with_primary_renderer<T>(
&mut self,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> Option<T> {
match self {
Backend::Tty(tty) => tty.renderer(),
Backend::Winit(winit) => Some(winit.renderer()),
Backend::Tty(tty) => tty.with_primary_renderer(f),
Backend::Winit(winit) => winit.with_primary_renderer(f),
}
}
@@ -56,12 +62,11 @@ impl Backend {
&mut self,
niri: &mut Niri,
output: &Output,
elements: &[OutputRenderElements<GlesRenderer>],
target_presentation_time: Duration,
) -> RenderResult {
match self {
Backend::Tty(tty) => tty.render(niri, output, elements, target_presentation_time),
Backend::Winit(winit) => winit.render(niri, output, elements),
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
Backend::Winit(winit) => winit.render(niri, output),
}
}
@@ -93,11 +98,24 @@ impl Backend {
}
}
#[cfg_attr(not(feature = "dbus"), allow(unused))]
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self {
Backend::Tty(tty) => tty.connectors(),
Backend::Winit(winit) => winit.connectors(),
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
}
}
pub fn early_import(&mut self, surface: &WlSurface) {
match self {
Backend::Tty(tty) => tty.early_import(surface),
Backend::Winit(_) => (),
}
}
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
match self {
Backend::Tty(tty) => tty.ipc_outputs(),
Backend::Winit(winit) => winit.ipc_outputs(),
}
}
@@ -107,21 +125,21 @@ impl Backend {
) -> Option<smithay::backend::allocator::gbm::GbmDevice<smithay::backend::drm::DrmDeviceFd>>
{
match self {
Backend::Tty(tty) => tty.gbm_device(),
Backend::Tty(tty) => tty.primary_gbm_device(),
Backend::Winit(_) => None,
}
}
pub fn is_active(&self) -> bool {
pub fn set_monitors_active(&mut self, active: bool) {
match self {
Backend::Tty(tty) => tty.is_active(),
Backend::Winit(_) => true,
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Winit(_) => (),
}
}
pub fn set_monitors_active(&self, active: bool) {
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.set_monitors_active(active),
Backend::Tty(tty) => tty.on_output_config_changed(niri),
Backend::Winit(_) => (),
}
}
+1436 -367
View File
File diff suppressed because it is too large Load Diff
+91 -62
View File
@@ -5,46 +5,41 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::Config;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{DebugFlags, Renderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
use smithay::backend::winit::{self, WinitEvent, WinitGraphicsBackend};
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::window::WindowBuilder;
use smithay::utils::Transform;
use super::RenderResult;
use crate::config::Config;
use crate::niri::{OutputRenderElements, RedrawState, State};
use crate::utils::get_monotonic_time;
use crate::Niri;
use super::{IpcOutputMap, RenderResult};
use crate::niri::{Niri, RedrawState, State};
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,
connectors: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
impl Winit {
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
pub fn new(
config: Rc<RefCell<Config>>,
event_loop: LoopHandle<State>,
) -> Result<Self, winit::Error> {
let builder = WindowBuilder::new()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri");
let (backend, winit) = winit::init_from_builder(builder).unwrap();
let output_config = config
.borrow()
.outputs
.iter()
.find(|o| o.name == "winit")
.cloned()
.unwrap_or_default();
let (backend, winit) = winit::init_from_builder(builder)?;
let output = Output::new(
"winit".to_string(),
@@ -60,18 +55,26 @@ impl Winit {
size: backend.window_size(),
refresh: 60_000,
};
let scale = output_config.scale.clamp(1., 10.).ceil() as i32;
output.change_current_state(
Some(mode),
Some(Transform::Flipped180),
Some(Scale::Integer(scale)),
None,
);
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
let connectors = Arc::new(Mutex::new(HashMap::from([(
let physical_properties = output.physical_properties();
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
output.clone(),
niri_ipc::Output {
name: output.name(),
make: physical_properties.make,
model: physical_properties.model,
physical_size: None,
modes: vec![niri_ipc::Mode {
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16,
refresh_rate: 60_000,
is_preferred: true,
}],
current_mode: Some(0),
logical: Some(logical_output(&output)),
},
)])));
let damage_tracker = OutputDamageTracker::from_output(&output);
@@ -89,41 +92,46 @@ impl Winit {
None,
None,
);
state.niri.output_resized(winit.output.clone());
{
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);
}
WinitEvent::Input(event) => state.process_input_event(event),
WinitEvent::Focus(_) => (),
WinitEvent::Redraw => state
.niri
.queue_redraw(state.backend.winit().output.clone()),
WinitEvent::CloseRequested => {
state.niri.stop_signal.stop();
state.niri.remove_output(&state.backend.winit().output);
}
WinitEvent::Redraw => state.niri.queue_redraw(&state.backend.winit().output),
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
})
.unwrap();
Self {
Ok(Self {
config,
output,
backend,
damage_tracker,
connectors,
}
ipc_outputs,
})
}
pub fn init(&mut self, niri: &mut Niri) {
// For some reason, binding the display here causes damage tracker artifacts.
//
// use smithay::backend::renderer::ImportEgl;
//
// if let Err(err) = self
// .backend
// .renderer()
// .bind_wl_display(&niri.display_handle)
// {
// warn!("error binding renderer wl_display: {err}");
// }
let renderer = self.backend.renderer();
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding renderer wl_display: {err}");
}
shaders::init(renderer);
niri.add_output(self.output.clone(), None);
}
@@ -131,23 +139,30 @@ impl Winit {
"winit".to_owned()
}
pub fn renderer(&mut self) -> &mut GlesRenderer {
self.backend.renderer()
pub fn with_primary_renderer<T>(
&mut self,
f: impl FnOnce(&mut GlesRenderer) -> T,
) -> Option<T> {
Some(f(self.backend.renderer()))
}
pub fn render(
&mut self,
niri: &mut Niri,
output: &Output,
elements: &[OutputRenderElements<GlesRenderer>],
) -> RenderResult {
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
// Hand them over to winit.
self.backend.bind().unwrap();
let age = self.backend.buffer_age().unwrap();
let res = self
.damage_tracker
.render_output(self.backend.renderer(), age, elements, [0.; 4])
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
.unwrap();
niri.update_primary_scanout_output(output, &res.states);
@@ -184,12 +199,16 @@ 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!(),
}
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
// FIXME: this should wait until a frame callback from the host compositor, but it redraws
// right away instead.
if output_state.unfinished_animations_remain {
self.backend.window().request_redraw();
}
@@ -202,7 +221,17 @@ impl Winit {
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
match self.backend.renderer().import_dmabuf(dmabuf, None) {
Ok(_texture) => true,
Err(err) => {
debug!("error importing dmabuf: {err:?}");
false
}
}
}
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
self.ipc_outputs.clone()
}
}
+64
View File
@@ -0,0 +1,64 @@
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use niri_ipc::Action;
use crate::utils::version;
#[derive(Parser)]
#[command(author, version = version(), about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_value_name = "SUBCOMMAND")]
#[command(subcommand_help_heading = "Subcommands")]
pub struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
pub config: Option<PathBuf>,
/// Import environment globally to systemd and D-Bus, run D-Bus services.
///
/// Set this flag in a systemd service started by your display manager, or when running
/// manually as your main compositor instance. Do not set when running as a nested window, or
/// on a TTY as your non-main compositor instance, to avoid messing up the global environment.
#[arg(long)]
pub session: bool,
/// Command to run upon compositor startup.
#[arg(last = true)]
pub command: Vec<OsString>,
#[command(subcommand)]
pub subcommand: Option<Sub>,
}
#[derive(Subcommand)]
pub enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
/// Communicate with the running niri instance.
Msg {
#[command(subcommand)]
msg: Msg,
/// Format output as JSON.
#[arg(short, long)]
json: bool,
},
/// Cause a panic to check if the backtraces are good.
Panic,
}
#[derive(Subcommand)]
pub enum Msg {
/// List connected outputs.
Outputs,
/// Print information about the focused window.
FocusedWindow,
/// Perform an action.
Action {
#[command(subcommand)]
action: Action,
},
}
-770
View File
@@ -1,770 +0,0 @@
use std::path::PathBuf;
use std::str::FromStr;
use bitflags::bitflags;
use directories::ProjectDirs;
use miette::{miette, Context, IntoDiagnostic};
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::Keysym;
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
#[knuffel(child, default)]
pub input: Input,
#[knuffel(children(name = "output"))]
pub outputs: Vec<Output>,
#[knuffel(children(name = "spawn-at-startup"))]
pub spawn_at_startup: Vec<SpawnAtStartup>,
#[knuffel(child, default)]
pub focus_ring: FocusRing,
#[knuffel(child, default)]
pub prefer_no_csd: bool,
#[knuffel(child, default)]
pub cursor: Cursor,
#[knuffel(child, unwrap(children), default)]
pub preset_column_widths: Vec<PresetWidth>,
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
#[knuffel(child, unwrap(argument), default = 16)]
pub gaps: u16,
#[knuffel(
child,
unwrap(argument),
default = Some(String::from(
"~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
)))
]
pub screenshot_path: Option<String>,
#[knuffel(child, default)]
pub binds: Binds,
#[knuffel(child, default)]
pub debug: DebugConfig,
}
// FIXME: Add other devices.
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Input {
#[knuffel(child, default)]
pub keyboard: Keyboard,
#[knuffel(child, default)]
pub touchpad: Touchpad,
#[knuffel(child, default)]
pub tablet: Tablet,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
pub struct Keyboard {
#[knuffel(child, default)]
pub xkb: Xkb,
// The defaults were chosen to match wlroots and sway.
#[knuffel(child, unwrap(argument), default = 600)]
pub repeat_delay: u16,
#[knuffel(child, unwrap(argument), default = 25)]
pub repeat_rate: u8,
#[knuffel(child, unwrap(argument), default)]
pub track_layout: TrackLayout,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
pub struct Xkb {
#[knuffel(child, unwrap(argument), default)]
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 variant: String,
#[knuffel(child, unwrap(argument))]
pub options: Option<String>,
}
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)]
pub enum TrackLayout {
/// The layout change is global.
#[default]
Global,
/// The layout change is window local.
Window,
}
// FIXME: Add the rest of the settings.
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Touchpad {
#[knuffel(child)]
pub tap: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: f64,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Tablet {
#[knuffel(child, unwrap(argument))]
pub map_to_output: Option<String>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Output {
#[knuffel(argument)]
pub name: String,
#[knuffel(child, unwrap(argument), default = 1.)]
pub scale: f64,
#[knuffel(child)]
pub position: Option<Position>,
#[knuffel(child, unwrap(argument, str))]
pub mode: Option<Mode>,
}
impl Default for Output {
fn default() -> Self {
Self {
name: String::new(),
scale: 1.,
position: None,
mode: None,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct Position {
#[knuffel(property)]
pub x: i32,
#[knuffel(property)]
pub y: i32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Mode {
pub width: u16,
pub height: u16,
pub refresh: Option<f64>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct SpawnAtStartup {
#[knuffel(arguments)]
pub command: Vec<String>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct FocusRing {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = 4)]
pub width: u16,
#[knuffel(child, default = Color::new(127, 200, 255, 255))]
pub active_color: Color,
#[knuffel(child, default = Color::new(80, 80, 80, 255))]
pub inactive_color: Color,
}
impl Default for FocusRing {
fn default() -> Self {
Self {
off: false,
width: 4,
active_color: Color::new(127, 200, 255, 255),
inactive_color: Color::new(80, 80, 80, 255),
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Color {
#[knuffel(argument)]
pub r: u8,
#[knuffel(argument)]
pub g: u8,
#[knuffel(argument)]
pub b: u8,
#[knuffel(argument)]
pub a: u8,
}
impl Color {
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
}
impl From<Color> for [f32; 4] {
fn from(c: Color) -> Self {
[c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.)
}
}
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Cursor {
#[knuffel(child, unwrap(argument), default = String::from("default"))]
pub xcursor_theme: String,
#[knuffel(child, unwrap(argument), default = 24)]
pub xcursor_size: u8,
}
impl Default for Cursor {
fn default() -> Self {
Self {
xcursor_theme: String::from("default"),
xcursor_size: 24,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub enum PresetWidth {
Proportion(#[knuffel(argument)] f64),
Fixed(#[knuffel(argument)] i32),
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct DefaultColumnWidth(#[knuffel(children)] pub Vec<PresetWidth>);
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Bind {
#[knuffel(node_name)]
pub key: Key,
#[knuffel(children)]
pub actions: Vec<Action>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Key {
pub keysym: Keysym,
pub modifiers: Modifiers,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Modifiers : u8 {
const CTRL = 1;
const SHIFT = 2;
const ALT = 4;
const SUPER = 8;
const COMPOSITOR = 16;
}
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub enum Action {
Quit,
#[knuffel(skip)]
ChangeVt(i32),
Suspend,
PowerOffMonitors,
ToggleDebugTint,
Spawn(#[knuffel(arguments)] Vec<String>),
#[knuffel(skip)]
ConfirmScreenshot,
#[knuffel(skip)]
CancelScreenshot,
Screenshot,
ScreenshotScreen,
ScreenshotWindow,
CloseWindow,
FullscreenWindow,
FocusColumnLeft,
FocusColumnRight,
FocusWindowDown,
FocusWindowUp,
MoveColumnLeft,
MoveColumnRight,
MoveWindowDown,
MoveWindowUp,
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[knuffel(argument)] u8),
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
MoveWorkspaceDown,
MoveWorkspaceUp,
FocusMonitorLeft,
FocusMonitorRight,
FocusMonitorDown,
FocusMonitorUp,
MoveWindowToMonitorLeft,
MoveWindowToMonitorRight,
MoveWindowToMonitorDown,
MoveWindowToMonitorUp,
SetWindowHeight(#[knuffel(argument, str)] SizeChange),
SwitchPresetColumnWidth,
MaximizeColumn,
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
SwitchLayout(#[knuffel(argument)] LayoutAction),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
SetFixed(i32),
SetProportion(f64),
AdjustFixed(i32),
AdjustProportion(f64),
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
pub enum LayoutAction {
Next,
Prev,
}
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct DebugConfig {
#[knuffel(child, unwrap(argument), default = 1.)]
pub animation_slowdown: f64,
#[knuffel(child)]
pub dbus_interfaces_in_non_session_instances: bool,
#[knuffel(child)]
pub wait_for_frame_completion_before_queueing: bool,
#[knuffel(child)]
pub enable_color_transformations_capability: bool,
#[knuffel(child)]
pub enable_overlay_planes: bool,
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
animation_slowdown: 1.,
dbus_interfaces_in_non_session_instances: false,
wait_for_frame_completion_before_queueing: false,
enable_color_transformations_capability: false,
enable_overlay_planes: false,
}
}
}
impl Config {
pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> {
let path = if let Some(path) = path {
path
} else {
let mut path = ProjectDirs::from("", "", "niri")
.ok_or_else(|| miette!("error retrieving home directory"))?
.config_dir()
.to_owned();
path.push("config.kdl");
path
};
let contents = std::fs::read_to_string(&path)
.into_diagnostic()
.with_context(|| format!("error reading {path:?}"))?;
let config = Self::parse("config.kdl", &contents).context("error parsing")?;
debug!("loaded config from {path:?}");
Ok((config, path))
}
pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
knuffel::parse(filename, text)
}
}
impl Default for Config {
fn default() -> Self {
Config::parse(
"default-config.kdl",
include_str!("../resources/default-config.kdl"),
)
.unwrap()
}
}
impl FromStr for Mode {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((width, rest)) = s.split_once('x') else {
return Err(miette!("no 'x' separator found"));
};
let (height, refresh) = match rest.split_once('@') {
Some((height, refresh)) => (height, Some(refresh)),
None => (rest, None),
};
let width = width
.parse()
.into_diagnostic()
.context("error parsing width")?;
let height = height
.parse()
.into_diagnostic()
.context("error parsing height")?;
let refresh = refresh
.map(str::parse)
.transpose()
.into_diagnostic()
.context("error parsing refresh rate")?;
Ok(Self {
width,
height,
refresh,
})
}
}
impl FromStr for Key {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut modifiers = Modifiers::empty();
let mut split = s.split('+');
let key = split.next_back().unwrap();
for part in split {
let part = part.trim();
if part.eq_ignore_ascii_case("mod") {
modifiers |= Modifiers::COMPOSITOR
} else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") {
modifiers |= Modifiers::CTRL;
} else if part.eq_ignore_ascii_case("shift") {
modifiers |= Modifiers::SHIFT;
} else if part.eq_ignore_ascii_case("alt") {
modifiers |= Modifiers::ALT;
} else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") {
modifiers |= Modifiers::SUPER;
} 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}"));
}
Ok(Key { keysym, modifiers })
}
}
impl FromStr for SizeChange {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err(miette!("trailing characters after '%' are not allowed"));
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err(miette!("value is missing")),
}
}
None => {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err(miette!("value is missing")),
}
}
}
}
}
#[cfg(test)]
mod tests {
use miette::NarratableReportHandler;
use super::*;
#[track_caller]
fn check(text: &str, expected: Config) {
let _ = miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new())));
let parsed = Config::parse("test.kdl", text)
.map_err(miette::Report::new)
.unwrap();
assert_eq!(parsed, expected);
}
#[test]
fn parse() {
check(
r#"
input {
keyboard {
repeat-delay 600
repeat-rate 25
track-layout "window"
xkb {
layout "us,ru"
options "grp:win_space_toggle"
}
}
touchpad {
tap
accel-speed 0.2
}
tablet {
map-to-output "eDP-1"
}
}
output "eDP-1" {
scale 2.0
position x=10 y=20
mode "1920x1080@144"
}
spawn-at-startup "alacritty" "-e" "fish"
focus-ring {
width 5
active-color 0 100 200 255
inactive-color 255 200 100 0
}
prefer-no-csd
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 16
}
preset-column-widths {
proportion 0.25
proportion 0.5
fixed 960
fixed 1280
}
default-column-width { proportion 0.25; }
gaps 8
screenshot-path "~/Screenshots/screenshot.png"
binds {
Mod+T { spawn "alacritty"; }
Mod+Q { close-window; }
Mod+Shift+H { focus-monitor-left; }
Mod+Ctrl+Shift+L { move-window-to-monitor-right; }
Mod+Comma { consume-window-into-column; }
Mod+1 { focus-workspace 1;}
}
debug {
animation-slowdown 2.0
}
"#,
Config {
input: Input {
keyboard: Keyboard {
xkb: Xkb {
layout: Some("us,ru".to_owned()),
options: Some("grp:win_space_toggle".to_owned()),
..Default::default()
},
repeat_delay: 600,
repeat_rate: 25,
track_layout: TrackLayout::Window,
},
touchpad: Touchpad {
tap: true,
natural_scroll: false,
accel_speed: 0.2,
},
tablet: Tablet {
map_to_output: Some("eDP-1".to_owned()),
},
},
outputs: vec![Output {
name: "eDP-1".to_owned(),
scale: 2.,
position: Some(Position { x: 10, y: 20 }),
mode: Some(Mode {
width: 1920,
height: 1080,
refresh: Some(144.),
}),
}],
spawn_at_startup: vec![SpawnAtStartup {
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
}],
focus_ring: FocusRing {
off: false,
width: 5,
active_color: Color {
r: 0,
g: 100,
b: 200,
a: 255,
},
inactive_color: Color {
r: 255,
g: 200,
b: 100,
a: 0,
},
},
prefer_no_csd: true,
cursor: Cursor {
xcursor_theme: String::from("breeze_cursors"),
xcursor_size: 16,
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
PresetWidth::Proportion(0.5),
PresetWidth::Fixed(960),
PresetWidth::Fixed(1280),
],
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(0.25)])),
gaps: 8,
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
binds: Binds(vec![
Bind {
key: Key {
keysym: Keysym::t,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::Spawn(vec!["alacritty".to_owned()])],
},
Bind {
key: Key {
keysym: Keysym::q,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::CloseWindow],
},
Bind {
key: Key {
keysym: Keysym::h,
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
actions: vec![Action::FocusMonitorLeft],
},
Bind {
key: Key {
keysym: Keysym::l,
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL,
},
actions: vec![Action::MoveWindowToMonitorRight],
},
Bind {
key: Key {
keysym: Keysym::comma,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::ConsumeWindowIntoColumn],
},
Bind {
key: Key {
keysym: Keysym::_1,
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::FocusWorkspace(1)],
},
]),
debug: DebugConfig {
animation_slowdown: 2.,
..Default::default()
},
},
);
}
#[test]
fn can_create_default_config() {
let _ = Config::default();
}
#[test]
fn parse_mode() {
assert_eq!(
"2560x1600@165.004".parse::<Mode>().unwrap(),
Mode {
width: 2560,
height: 1600,
refresh: Some(165.004),
},
);
assert_eq!(
"1920x1080".parse::<Mode>().unwrap(),
Mode {
width: 1920,
height: 1080,
refresh: None,
},
);
assert!("1920".parse::<Mode>().is_err());
assert!("1920x".parse::<Mode>().is_err());
assert!("1920x1080@".parse::<Mode>().is_err());
assert!("1920x1080@60Hz".parse::<Mode>().is_err());
}
#[test]
fn parse_size_change() {
assert_eq!(
"10".parse::<SizeChange>().unwrap(),
SizeChange::SetFixed(10),
);
assert_eq!(
"+10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(10),
);
assert_eq!(
"-10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(-10),
);
assert_eq!(
"10%".parse::<SizeChange>().unwrap(),
SizeChange::SetProportion(10.),
);
assert_eq!(
"+10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(10.),
);
assert_eq!(
"-10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(-10.),
);
assert!("-".parse::<SizeChange>().is_err());
assert!("10% ".parse::<SizeChange>().is_err());
}
}
+5 -12
View File
@@ -8,8 +8,7 @@ use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureBuffer;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
@@ -224,7 +223,7 @@ pub enum RenderCursor {
},
}
type TextureCache = HashMap<(CursorIcon, i32), Vec<TextureBuffer<GlesTexture>>>;
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
#[derive(Default)]
pub struct CursorTextureCache {
@@ -238,12 +237,11 @@ impl CursorTextureCache {
pub fn get(
&self,
renderer: &mut GlesRenderer,
icon: CursorIcon,
scale: i32,
cursor: &XCursor,
idx: usize,
) -> TextureBuffer<GlesTexture> {
) -> MemoryRenderBuffer {
self.cache
.borrow_mut()
.entry((icon, scale))
@@ -252,19 +250,14 @@ impl CursorTextureCache {
.frames()
.iter()
.map(|frame| {
let _span = tracy_client::span!("create TextureBuffer");
TextureBuffer::from_memory(
renderer,
MemoryRenderBuffer::from_slice(
&frame.pixels_rgba,
Fourcc::Abgr8888,
Fourcc::Argb8888,
(frame.width as i32, frame.height as i32),
false,
scale,
Transform::Normal,
None,
)
.unwrap()
})
.collect()
})[idx]
+166
View File
@@ -0,0 +1,166 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use anyhow::Context;
use futures_util::StreamExt;
use zbus::fdo::{self, RequestNameFlags};
use zbus::names::{OwnedUniqueName, UniqueName};
use zbus::zvariant::NoneValue;
use zbus::{dbus_interface, MessageHeader, Task};
use super::Start;
pub struct ScreenSaver {
is_inhibited: Arc<AtomicBool>,
is_broken: Arc<AtomicBool>,
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
counter: u32,
monitor_task: Arc<OnceLock<Task<()>>>,
}
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
impl ScreenSaver {
async fn inhibit(
&mut self,
#[zbus(header)] hdr: MessageHeader<'_>,
application_name: &str,
reason_for_inhibit: &str,
) -> fdo::Result<u32> {
trace!(
"fdo inhibit, app: `{application_name}`, reason: `{reason_for_inhibit}`, owner: {:?}",
hdr.sender()
);
let Ok(Some(name)) = hdr.sender() else {
return Err(fdo::Error::Failed(String::from("no sender")));
};
let name = OwnedUniqueName::from(name.to_owned());
let mut inhibitors = self.inhibitors.lock().unwrap();
let mut cookie = None;
for _ in 0..3 {
// Start from 1 because some clients don't like 0.
self.counter = self.counter.wrapping_add(1);
if self.counter == 0 {
self.counter += 1;
}
if let Entry::Vacant(entry) = inhibitors.entry(self.counter) {
entry.insert(name);
self.is_inhibited.store(true, Ordering::SeqCst);
cookie = Some(self.counter);
break;
}
}
cookie.ok_or_else(|| fdo::Error::Failed(String::from("no available cookie")))
}
async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> {
trace!("fdo uninhibit, cookie: {cookie}");
let mut inhibitors = self.inhibitors.lock().unwrap();
if inhibitors.remove(&cookie).is_some() {
if inhibitors.is_empty() {
self.is_inhibited.store(false, Ordering::SeqCst);
}
Ok(())
} else {
Err(fdo::Error::Failed(String::from("invalid cookie")))
}
}
}
impl ScreenSaver {
pub fn new(is_inhibited: Arc<AtomicBool>) -> Self {
Self {
is_inhibited,
is_broken: Arc::new(AtomicBool::new(false)),
inhibitors: Arc::new(Mutex::new(HashMap::new())),
counter: 0,
monitor_task: Arc::new(OnceLock::new()),
}
}
}
async fn monitor_disappeared_clients(
conn: &zbus::Connection,
is_inhibited: Arc<AtomicBool>,
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
) -> anyhow::Result<()> {
let proxy = fdo::DBusProxy::new(conn)
.await
.context("error creating a DBusProxy")?;
let mut stream = proxy
.receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())])
.await
.context("error creating a NameOwnerChanged stream")?;
while let Some(signal) = stream.next().await {
let args = signal
.args()
.context("error retrieving NameOwnerChanged args")?;
let Some(name) = &**args.old_owner() else {
continue;
};
if args.new_owner().is_none() {
trace!("fdo ScreenSaver client disappeared: {name}");
let mut inhibitors = inhibitors.lock().unwrap();
inhibitors.retain(|_, owner| owner != name);
is_inhibited.store(!inhibitors.is_empty(), Ordering::SeqCst);
} else {
error!("non-null new_owner should've been filtered out");
}
}
Ok(())
}
impl Start for ScreenSaver {
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
let is_inhibited = self.is_inhibited.clone();
let is_broken = self.is_broken.clone();
let inhibitors = self.inhibitors.clone();
let monitor_task = self.monitor_task.clone();
let conn = zbus::blocking::Connection::session()?;
let flags = RequestNameFlags::AllowReplacement
| RequestNameFlags::ReplaceExisting
| RequestNameFlags::DoNotQueue;
conn.object_server()
.at("/org/freedesktop/ScreenSaver", self)?;
conn.request_name_with_flags("org.freedesktop.ScreenSaver", flags)?;
let async_conn = conn.inner();
let future = {
let conn = async_conn.clone();
async move {
if let Err(err) =
monitor_disappeared_clients(&conn, is_inhibited.clone(), inhibitors.clone())
.await
{
warn!("error monitoring org.freedesktop.ScreenSaver clients: {err:?}");
is_broken.store(true, Ordering::SeqCst);
is_inhibited.store(false, Ordering::SeqCst);
inhibitors.lock().unwrap().clear();
}
}
};
let task = async_conn
.executor()
.spawn(future, "monitor disappearing clients");
monitor_task.set(task).unwrap();
Ok(conn)
}
}
-1
View File
@@ -1,6 +1,5 @@
use std::path::PathBuf;
use smithay::reexports::calloop;
use zbus::dbus_interface;
use zbus::fdo::{self, RequestNameFlags};
+11 -4
View File
@@ -1,9 +1,9 @@
use smithay::reexports::calloop;
use zbus::blocking::Connection;
use zbus::Interface;
use crate::niri::State;
pub mod freedesktop_screensaver;
pub mod gnome_shell_screenshot;
pub mod mutter_display_config;
pub mod mutter_service_channel;
@@ -13,6 +13,7 @@ pub mod mutter_screen_cast;
#[cfg(feature = "xdp-gnome-screencast")]
use mutter_screen_cast::ScreenCast;
use self::freedesktop_screensaver::ScreenSaver;
use self::mutter_display_config::DisplayConfig;
use self::mutter_service_channel::ServiceChannel;
@@ -24,6 +25,7 @@ trait Start: Interface {
pub struct DBusServers {
pub conn_service_channel: Option<Connection>,
pub conn_display_config: Option<Connection>,
pub conn_screen_saver: Option<Connection>,
pub conn_screen_shot: Option<Connection>,
#[cfg(feature = "xdp-gnome-screencast")]
pub conn_screen_cast: Option<Connection>,
@@ -45,9 +47,12 @@ impl DBusServers {
}
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
let display_config = DisplayConfig::new(backend.connectors());
let display_config = DisplayConfig::new(backend.ipc_outputs());
dbus.conn_display_config = try_start(display_config);
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
dbus.conn_screen_saver = try_start(screen_saver);
let (to_niri, from_screenshot) = calloop::channel::channel();
let (to_screenshot, from_niri) = async_channel::unbounded();
niri.event_loop
@@ -62,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, {
@@ -75,8 +80,10 @@ impl DBusServers {
}
})
.unwrap();
let screen_cast = ScreenCast::new(backend.connectors(), 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");
}
}
+105 -27
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::{OwnedValue, Type};
use zbus::{dbus_interface, fdo};
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
pub struct DisplayConfig {
connectors: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
#[derive(Serialize, Type)]
@@ -53,40 +53,118 @@ impl DisplayConfig {
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let monitors: Vec<Monitor> = self
.connectors
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
.ipc_outputs
.lock()
.unwrap()
.keys()
.map(|c| Monitor {
names: (c.clone(), String::new(), String::new(), String::new()),
modes: vec![],
properties: HashMap::new(),
})
.collect();
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(),
// 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-"));
// FIXME: use proper serial when we have libdisplay-info.
// A serial is required for correct session restore by xdp-gnome.
let serial = c.clone();
let mut properties = HashMap::new();
if is_laptop_panel {
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
);
}
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
);
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,
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();
Ok((0, monitors, logical_monitors, HashMap::new()))
// Sort the built-in monitor first, then by connector name.
monitors.sort_unstable_by(|a, b| {
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.0.names.0.cmp(&b.0.names.0))
});
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))
}
// FIXME: monitors-changed signal.
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
pub fn new(connectors: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { connectors }
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
Self { ipc_outputs }
}
}
+48 -15
View File
@@ -5,26 +5,29 @@ use std::sync::{Arc, Mutex};
use serde::Deserialize;
use smithay::output::Output;
use smithay::reexports::calloop;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start;
use crate::backend::IpcOutputMap;
#[derive(Clone)]
pub struct ScreenCast {
connectors: 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>)>>>,
}
#[derive(Clone)]
pub struct Session {
id: usize,
connectors: 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)]
@@ -46,22 +49,33 @@ 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>,
}
#[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),
}
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
output: Output,
output: String,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
},
StopCast {
session_id: usize,
},
Redraw(Output),
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
@@ -82,7 +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.connectors.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();
@@ -122,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 {
@@ -149,10 +168,14 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.connectors.lock().unwrap().get(connector).cloned() else {
let Some(output) = self.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{}",
@@ -162,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();
@@ -188,15 +211,24 @@ impl Stream {
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
-> zbus::Result<()>;
#[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters {
let 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(
connectors: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
connectors,
ipc_outputs,
to_niri,
sessions: Arc::new(Mutex::new(vec![])),
}
@@ -221,14 +253,15 @@ impl Start for ScreenCast {
impl Session {
pub fn new(
id: usize,
connectors: Arc<Mutex<HashMap<String, Output>>>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self {
Self {
id,
connectors,
ipc_outputs,
streams: Arc::new(Mutex::new(vec![])),
to_niri,
stopped: Arc::new(AtomicBool::new(false)),
}
}
}
@@ -243,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 {
@@ -262,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,
};
+7 -3
View File
@@ -25,9 +25,13 @@ impl ServiceChannel {
}
let (sock1, sock2) = UnixStream::pair().unwrap();
self.display
.insert_client(sock2, Arc::new(ClientState::default()))
.unwrap();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
// Would be nice to thread config here but for now it's fine.
can_view_decoration_globals: false,
restricted: false,
});
self.display.insert_client(sock2, data).unwrap();
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
}
}
+109 -29
View File
@@ -1,7 +1,6 @@
use std::collections::hash_map::Entry;
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
use smithay::desktop::find_popup_root_surface;
use smithay::input::pointer::CursorImageStatus;
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
@@ -17,8 +16,8 @@ use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use super::xdg_shell;
use crate::niri::{ClientState, State};
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@@ -30,7 +29,12 @@ impl CompositorHandler for State {
}
fn new_subsurface(&mut self, surface: &WlSurface, parent: &WlSurface) {
if let Some((_, output)) = self.niri.layout.find_window_and_output(parent) {
let mut root = parent.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
if let Some(output) = self.niri.output_for_root(&root) {
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
with_states(surface, |data| {
@@ -70,13 +74,14 @@ impl CompositorHandler for State {
}
}
}
})
});
}
fn commit(&mut self, surface: &WlSurface) {
let _span = tracy_client::span!("CompositorHandler::commit");
on_commit_buffer_handler::<Self>(surface);
self.backend.early_import(surface);
if is_sync_subsurface(surface) {
return;
@@ -91,51 +96,127 @@ impl CompositorHandler for State {
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some());
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if is_mapped {
// The toplevel got mapped.
let window = entry.remove();
let Unmapped { window, state } = entry.remove();
window.on_commit();
if let Some(output) = self
.niri
.layout
.add_window(window, true, None, false)
.cloned()
{
self.niri.queue_redraw(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());
(rules, width, is_full_width, output)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None)
};
let parent = window
.toplevel()
.expect("no x11 support")
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
// Only consider the parent if we configured the window for the same
// output.
//
// Normally when we're following the parent, the configured output will be
// None. If the configured output is set, that means it was set explicitly
// by a window rule or a fullscreen request.
.filter(|(_, parent_output)| {
output.is_none() || output.as_ref() == Some(*parent_output)
})
.map(|(mapped, _)| mapped.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, mapped, width, is_full_width)
} else if let Some(output) = &output {
self.niri
.layout
.add_window_on_output(output, mapped, width, is_full_width);
Some(output)
} else {
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);
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;
}
// The toplevel remains unmapped.
let window = entry.get();
xdg_shell::send_initial_configure_if_needed(window.toplevel());
let unmapped = entry.get();
if unmapped.needs_initial_configure() {
let toplevel = unmapped.window.toplevel().expect("no x11 support").clone();
self.queue_initial_configure(toplevel);
}
return;
}
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some((window, output)) = self.niri.layout.find_window_and_output(surface) {
// This is a commit of a previously-mapped toplevel.
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();
// This is a commit of a previously-mapped toplevel.
let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some());
with_renderer_surface_state(surface, |state| state.buffer().is_some())
.unwrap_or_else(|| {
error!("no renderer surface state even though we use commit handler");
false
});
if !is_mapped {
// The toplevel got unmapped.
self.niri.layout.remove_window(&window);
self.niri.unmapped_windows.insert(surface.clone(), window);
self.niri.queue_redraw(output);
// Newly-unmapped toplevels must perform the initial commit-configure sequence
// afresh.
let unmapped = Unmapped::new(window);
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
self.niri.queue_redraw(&output);
return;
}
// The toplevel remains mapped.
self.niri.layout.update_window(&window);
self.niri.queue_redraw(output);
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window, &output);
self.niri.queue_redraw(&output);
return;
}
@@ -144,21 +225,20 @@ 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 {
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;
}
// This might be a popup.
self.popups_handle_commit(surface);
if let Some(popup) = self.niri.popups.find_popup(surface) {
if let Ok(root) = find_popup_root_surface(&popup) {
let root_window_output = self.niri.layout.find_window_and_output(&root);
if let Some((_window, output)) = root_window_output {
self.niri.queue_redraw(output);
}
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(&output.clone());
}
}
@@ -183,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;
}
}
+7 -2
View File
@@ -8,6 +8,7 @@ use smithay::wayland::shell::wlr_layer::{
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
WlrLayerShellState,
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::niri::State;
@@ -49,9 +50,13 @@ impl WlrLayerShellHandler for State {
None
};
if let Some(output) = output {
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
self.unconstrain_popup(&popup);
}
}
delegate_layer_shell!(State);
@@ -102,6 +107,6 @@ impl State {
}
drop(map);
self.niri.output_resized(output);
self.niri.output_resized(&output);
}
}
+252 -26
View File
@@ -9,11 +9,12 @@ use std::sync::Arc;
use std::thread;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::ImportDma;
use smithay::backend::drm::DrmNode;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -21,7 +22,17 @@ use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Logical, Rectangle, Size};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::security_context::{
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
};
use smithay::wayland::selection::data_device::{
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
ServerDndGrabHandler,
@@ -36,17 +47,26 @@ use smithay::wayland::session_lock::{
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_input_method_manager, delegate_output, delegate_pointer_gestures,
delegate_presentation, delegate_primary_selection, delegate_seat, delegate_session_lock,
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
delegate_security_context, delegate_session_lock, delegate_tablet_manager,
delegate_text_input_manager, delegate_viewporter, delegate_virtual_keyboard_manager,
};
use crate::layout::output_size;
use crate::niri::State;
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_gamma_control, delegate_screencopy};
impl SeatHandler for State {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<State> {
&mut self.niri.seat_state
@@ -69,19 +89,41 @@ impl SeatHandler for State {
set_data_device_focus(dh, seat, client.clone());
set_primary_focus(dh, seat, client);
}
fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: keyboard::LedState) {
let keyboards = self
.niri
.devices
.iter()
.filter(|device| device.has_capability(input::DeviceCapability::Keyboard))
.cloned();
for mut keyboard in keyboards {
keyboard.led_update(led_state.into());
}
}
}
delegate_seat!(State);
delegate_cursor_shape!(State);
delegate_tablet_manager!(State);
delegate_pointer_gestures!(State);
delegate_relative_pointer!(State);
delegate_text_input_manager!(State);
impl PointerConstraintsHandler for State {
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
self.niri.maybe_activate_pointer_constraint(
pointer.current_location(),
&self.niri.pointer_focus,
);
}
}
delegate_pointer_constraints!(State);
impl InputMethodHandler for State {
fn new_popup(&mut self, surface: PopupSurface) {
if let Some((_, output)) = surface
.get_parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent.surface))
{
let popup = PopupKind::from(surface.clone());
if let Some(output) = self.output_for_popup(&popup) {
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
let wl_surface = surface.wl_surface();
@@ -89,7 +131,7 @@ impl InputMethodHandler for State {
send_surface_state(wl_surface, data, scale, transform);
});
}
if let Err(err) = self.niri.popups.track_popup(PopupKind::from(surface)) {
if let Err(err) = self.niri.popups.track_popup(popup) {
warn!("error tracking ime popup {err:?}");
}
}
@@ -102,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()
}
}
@@ -176,13 +218,18 @@ impl DataControlHandler for State {
delegate_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
}
}
delegate_output!(State);
delegate_presentation!(State);
impl DmabufHandler for State {
fn dmabuf_state(&mut self) -> &mut DmabufState {
self.backend.tty().dmabuf_state()
&mut self.niri.dmabuf_state
}
fn dmabuf_imported(
@@ -191,17 +238,10 @@ impl DmabufHandler for State {
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
let renderer = self.backend.renderer().expect(
"the dmabuf global must be created and destroyed together with the output device",
);
match renderer.import_dmabuf(&dmabuf, None) {
Ok(_texture) => {
let _ = notifier.successful::<State>();
}
Err(err) => {
debug!("error importing dmabuf: {err:?}");
notifier.failed();
}
if self.backend.import_dmabuf(&dmabuf) {
let _ = notifier.successful::<State>();
} else {
notifier.failed();
}
}
}
@@ -245,3 +285,189 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
});
surface.send_configure();
}
impl SecurityContextHandler for State {
fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
self.niri
.event_loop
.insert_source(source, move |client, _, state| {
let config = state.niri.config.borrow();
let data = Arc::new(ClientState {
compositor_state: Default::default(),
can_view_decoration_globals: config.prefer_no_csd,
restricted: true,
});
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
warn!("error inserting client: {err}");
} else {
trace!("inserted a new restricted client, context={context:?}");
}
})
.unwrap();
}
}
delegate_security_context!(State);
impl IdleNotifierHandler for State {
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
&mut self.niri.idle_notifier_state
}
}
delegate_idle_notify!(State);
impl IdleInhibitHandler for State {
fn inhibit(&mut self, surface: WlSurface) {
self.niri.idle_inhibiting_surfaces.insert(surface);
}
fn uninhibit(&mut self, surface: WlSurface) {
self.niri.idle_inhibiting_surfaces.remove(&surface);
}
}
delegate_idle_inhibit!(State);
impl ForeignToplevelHandler for State {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
&mut self.niri.foreign_toplevel_state
}
fn activate(&mut self, wl_surface: WlSurface) {
if let Some((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((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((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !mapped
.toplevel()
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
return;
}
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, &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
}
}
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
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);
}
}
}
delegate_foreign_toplevel!(State);
impl ScreencopyHandler for State {
fn frame(&mut self, screencopy: Screencopy) {
if let Err(err) = self
.niri
.render_for_screencopy(&mut self.backend, screencopy)
{
warn!("error rendering for screencopy: {err:?}");
}
}
}
delegate_screencopy!(State);
impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
&mut self
.backend
.tty()
.get_device_from_node(node)
.unwrap()
.drm_lease_state
}
fn lease_request(
&mut self,
node: DrmNode,
request: DrmLeaseRequest,
) -> Result<DrmLeaseBuilder, LeaseRejected> {
debug!(
"Received lease request for {} connectors",
request.connectors.len()
);
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.lease_request(request)
}
fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) {
debug!("Lease success");
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.new_lease(lease);
}
fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) {
debug!("Destroyed lease");
self.backend
.tty()
.get_device_from_node(node)
.unwrap()
.remove_lease(lease_id);
}
}
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);
+607 -100
View File
@@ -1,21 +1,34 @@
use smithay::desktop::{find_popup_root_surface, PopupKind, Window};
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
WindowSurfaceType,
};
use smithay::input::pointer::Focus;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge};
use smithay::reexports::wayland_server::protocol::wl_output;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::Serial;
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::Layer;
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData,
};
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use crate::niri::State;
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
@@ -24,33 +37,14 @@ impl XdgShellHandler for State {
fn new_toplevel(&mut self, surface: ToplevelSurface) {
let wl_surface = surface.wl_surface().clone();
let window = Window::new(surface);
// Tell the surface the preferred size and bounds for its likely output.
if let Some(ws) = self.niri.layout.active_workspace() {
ws.configure_new_window(&window);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
let config = self.niri.config.borrow();
if config.prefer_no_csd {
window.toplevel().with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// At the moment of creation, xdg toplevels must have no buffer.
let existing = self.niri.unmapped_windows.insert(wl_surface, window);
let unmapped = Unmapped::new(Window::new_wayland_window(surface));
let existing = self.niri.unmapped_windows.insert(wl_surface, unmapped);
assert!(existing.is_none());
}
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
// FIXME: adjust the geometry so the popup doesn't overflow at least off the top and bottom
// screen edges, and ideally off the view size.
self.unconstrain_popup(&surface);
if let Err(err) = self.niri.popups.track_popup(PopupKind::Xdg(surface)) {
warn!("error tracking popup: {err:?}");
}
@@ -76,26 +70,121 @@ impl XdgShellHandler for State {
positioner: PositionerState,
token: u32,
) {
// FIXME: adjust the geometry so the popup doesn't overflow at least off the top and bottom
// screen edges, and ideally off the view size.
surface.with_pending_state(|state| {
let geometry = positioner.get_geometry();
state.geometry = geometry;
state.positioner = positioner;
});
self.unconstrain_popup(&surface);
surface.send_repositioned(token);
}
fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {
// FIXME popup grabs
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
// a grab. It will likely need refactors in Smithay to support properly since grabs just
// replace each other.
// FIXME: do this properly.
if self.niri.seat.input_method().keyboard_grabbed() {
trace!("ignoring popup grab because IME has keyboard grabbed");
return;
}
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
return;
};
// We need to hand out the grab in a way consistent with what update_keyboard_focus()
// thinks the current focus is, otherwise it will desync and cause weird issues with
// keyboard focus being at the wrong place.
if self.niri.is_locked() {
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else if self.niri.screenshot_ui.is_open() {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
} else if let Some(output) = self.niri.layout.active_output() {
let layers = layer_map_for_output(output);
if let Some(layer_surface) =
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
{
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
} else {
if layers
.layers_on(Layer::Overlay)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let mon = self.niri.layout.monitor_for_output(output).unwrap();
if !mon.render_above_top_layer()
&& layers
.layers_on(Layer::Top)
.any(|l| l.can_receive_keyboard_focus())
{
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let layout_focus = self.niri.layout.focus();
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
}
} else {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
let seat = &self.niri.seat;
let Ok(mut grab) = self
.niri
.popups
.grab_popup(root.clone(), popup, seat, serial)
else {
return;
};
let keyboard = seat.get_keyboard().unwrap();
let pointer = seat.get_pointer().unwrap();
let keyboard_grab_mismatches = keyboard.is_grabbed()
&& !(keyboard.has_grab(serial)
|| grab
.previous_serial()
.map_or(true, |s| keyboard.has_grab(s)));
let pointer_grab_mismatches = pointer.is_grabbed()
&& !(pointer.has_grab(serial)
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
if keyboard_grab_mismatches || pointer_grab_mismatches {
grab.ungrab(PopupUngrabStrategy::All);
return;
}
trace!("new grab for root {:?}", root);
keyboard.set_focus(self, grab.current_grab(), serial);
keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
self.niri.popup_grab = Some(PopupGrabState { root, grab });
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
// FIXME
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&surface) {
surface.send_configure();
}
}
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
@@ -104,46 +193,167 @@ impl XdgShellHandler for State {
fn fullscreen_request(
&mut self,
surface: ToplevelSurface,
toplevel: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
if surface
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
{
// NOTE: This is only one part of the solution. We can set the
// location and configure size here, but the surface should be rendered fullscreen
// independently from its buffer size
if let Some((window, current_output)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
{
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);
}
}
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
self.niri.layout.set_fullscreen(&window, true);
}
}
// The protocol demands us to always reply with a configure,
// regardless of we fulfilled the request or not
surface.send_configure();
}
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
if let Some((window, _)) = self
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output(surface.wl_surface())
.find_window_and_output(toplevel.wl_surface())
{
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, &requested_output);
}
}
self.niri.layout.set_fullscreen(&window, true);
// A configure is required in response to this event regardless if there are pending
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
*wants_fullscreen = Some(requested_output);
// The required configure will be the initial configure.
}
InitialConfigureState::Configured { output, .. } => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = requested_output
.as_ref()
// If none requested, try currently configured output.
.or(output.as_ref())
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
ws.configure_new_window(&unmapped.window, None);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in fullscreen_request()");
toplevel.send_configure();
}
}
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
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
// changes.
toplevel.send_configure();
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
match &mut unmapped.state {
InitialConfigureState::NotConfigured { wants_fullscreen } => {
*wants_fullscreen = None;
// The required configure will be the initial configure.
}
InitialConfigureState::Configured {
width,
is_full_width,
output,
..
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Fullscreen);
});
let configure_width = if *is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
*width
};
ws.configure_new_window(&unmapped.window, configure_width);
}
// We already sent the initial configure, so we need to reconfigure.
toplevel.send_configure();
}
}
} else {
error!("couldn't find the toplevel in unfullscreen_request()");
toplevel.send_configure();
}
}
@@ -163,65 +373,82 @@ impl XdgShellHandler for State {
.layout
.find_window_and_output(surface.wl_surface());
let Some((window, output)) = win_out 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 Ok(root) = find_popup_root_surface(&surface.into()) {
let root_window_output = self.niri.layout.find_window_and_output(&root);
if let Some((_window, output)) = root_window_output {
self.niri.queue_redraw(output);
}
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
self.niri.queue_redraw(&output.clone());
}
}
fn app_id_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
}
delegate_xdg_shell!(State);
impl XdgDecorationHandler for State {
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = mode;
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
}
fn request_mode(&mut self, toplevel: ToplevelSurface, mode: zxdg_toplevel_decoration_v1::Mode) {
// Set whatever the client wants, rather than our preferred mode. This especially matters
// for SDL2 which has a bug where forcing a different (client-side) decoration mode during
// their window creation sequence would leave the window permanently hidden.
//
// https://github.com/libsdl-org/SDL/issues/8173
//
// The bug has been fixed, but there's a ton of apps which will use the buggy version for a
// long while...
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(mode);
});
// Only send configure if it's non-initial.
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_pending_configure();
toplevel.send_configure();
}
}
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
let mode = if self.niri.config.borrow().prefer_no_csd {
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
} else {
None
};
// If we want CSD, we hide this global altogether.
toplevel.with_pending_state(|state| {
state.decoration_mode = mode;
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
// Only send configure if it's non-initial.
// A configure is required in response to this event. However, if an initial configure
// wasn't sent, then we will send this as part of the initial configure later.
if initial_configure_sent(&toplevel) {
toplevel.send_pending_configure();
toplevel.send_configure();
}
}
}
@@ -232,14 +459,14 @@ impl KdeDecorationHandler for State {
&self.niri.kde_decoration_state
}
}
delegate_kde_decoration!(State);
pub fn send_initial_configure_if_needed(toplevel: &ToplevelSurface) {
if !initial_configure_sent(toplevel) {
toplevel.send_configure();
impl XdgForeignHandler for State {
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
&mut self.niri.xdg_foreign_state
}
}
delegate_xdg_foreign!(State);
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
@@ -254,6 +481,132 @@ fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
}
impl State {
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
let _span = tracy_client::span!("State::send_initial_configure");
let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) else {
error!("window must be present in unmapped_windows in send_initial_configure()");
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 {
error!("window must not be already configured in send_initial_configure()");
return;
};
// Pick the target monitor. First, check if we had an output set in the window rules.
let mon = rules
.open_on_output
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|o| self.niri.layout.monitor_for_output(o));
// If not, check if the window requested one for fullscreen.
let mon = mon.or_else(|| {
wants_fullscreen
.as_ref()
.and_then(|x| x.as_ref())
// The monitor might not exist if the output was disconnected.
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
// If not, check if this is a dialog with a parent, to place it next to the parent.
let mon = mon.map(|mon| (mon, false)).or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
});
// If not, use the active monitor.
let mon = mon.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
// If we're following the parent, don't set the target output, so that when the window is
// mapped, it fetches the possibly changed parent's output again, and shows up there.
let output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let mut width = None;
let is_full_width = rules.open_maximized.unwrap_or(false);
// Tell the surface the preferred size and bounds for its likely output.
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
if let Some(ws) = ws {
// Set a fullscreen state based on window request and window rule.
if (wants_fullscreen.is_some() && rules.open_fullscreen.is_none())
|| rules.open_fullscreen == Some(true)
{
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
width = ws.resolve_default_width(rules.default_width);
let configure_width = if is_full_width {
Some(ColumnWidth::Proportion(1.))
} else {
width
};
ws.configure_new_window(window, configure_width);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
// rid of the various client-side rounded corners also by using the tiled state.
if config.prefer_no_csd {
toplevel.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::TiledLeft);
state.states.set(xdg_toplevel::State::TiledRight);
state.states.set(xdg_toplevel::State::TiledTop);
state.states.set(xdg_toplevel::State::TiledBottom);
});
}
// Set the configured settings.
*state = InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
};
toplevel.send_configure();
}
pub fn queue_initial_configure(&self, toplevel: ToplevelSurface) {
// Send the initial configure in an idle, in case the client sent some more info after the
// initial commit.
self.niri.event_loop.insert_idle(move |state| {
if !toplevel.alive() {
return;
}
if let Some(unmapped) = state.niri.unmapped_windows.get(toplevel.wl_surface()) {
if unmapped.needs_initial_configure() {
state.send_initial_configure(&toplevel);
}
}
});
}
/// Should be called on `WlSurface::commit`
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
self.niri.popups.commit(surface);
@@ -271,12 +624,8 @@ impl State {
.initial_configure_sent
});
if !initial_configure_sent {
if let Some(output) = popup.get_parent_surface().and_then(|parent| {
self.niri
.layout
.find_window_and_output(&parent)
.map(|(_, output)| output)
}) {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
{
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
with_states(surface, |data| {
@@ -291,4 +640,162 @@ impl State {
}
}
}
pub fn output_for_popup(&self, popup: &PopupKind) -> Option<&Output> {
let root = find_popup_root_surface(popup).ok()?;
self.niri.output_for_root(&root)
}
pub fn unconstrain_popup(&self, popup: &PopupSurface) {
let _span = tracy_client::span!("Niri::unconstrain_popup");
// Popups with a NULL parent will get repositioned in their respective protocol handlers
// (i.e. layer-shell).
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
return;
};
// Figure out if the root is a window or a layer surface.
if let Some((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)?;
Some((layer_surface.clone(), o))
}) {
self.unconstrain_layer_shell_popup(popup, &layer_surface, output);
}
}
fn unconstrain_window_popup(&self, popup: &PopupSurface, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
//
// We try to keep regular window popups within the window itself horizontally (since the
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h));
target.loc.y -= self.niri.layout.window_y(window).unwrap();
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
pub fn unconstrain_layer_shell_popup(
&self,
popup: &PopupSurface,
layer_surface: &LayerSurface,
output: &Output,
) {
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let map = layer_map_for_output(output);
let Some(layer_geo) = map.layer_geometry(layer_surface) else {
return;
};
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(
window.toplevel().expect("no x11 support").wl_surface(),
) {
match popup {
PopupKind::Xdg(ref popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(popup, window, output);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
}
}
PopupKind::InputMethod(_) => (),
}
}
}
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
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 = 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);
}
}
}
}
}
fn unconstrain_with_padding(
positioner: PositionerState,
target: Rectangle<i32, Logical>,
) -> Rectangle<i32, Logical> {
// Try unconstraining with a small padding first which looks nicer, then if it doesn't fit try
// unconstraining without padding.
const PADDING: i32 = 8;
let mut padded = target;
if PADDING * 2 < padded.size.w {
padded.loc.x += PADDING;
padded.size.w -= PADDING * 2;
}
if PADDING * 2 < padded.size.h {
padded.loc.y += PADDING;
padded.size.h -= PADDING * 2;
}
// No padding, so just unconstrain with the original target.
if padded == target {
return positioner.get_unconstrained_geometry(target);
}
// Do not try to resize to fit the padded target rectangle.
let mut no_resize = positioner;
no_resize
.constraint_adjustment
.remove(ConstraintAdjustment::ResizeX);
no_resize
.constraint_adjustment
.remove(ConstraintAdjustment::ResizeY);
let geo = no_resize.get_unconstrained_geometry(padded);
if padded.contains_rect(geo) {
return geo;
}
// Could not unconstrain into the padded target, so resort to the regular one.
positioner.get_unconstrained_geometry(target)
}
+2133 -876
View File
File diff suppressed because it is too large Load Diff
+188
View File
@@ -0,0 +1,188 @@
use std::env;
use std::io::{Read, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
use anyhow::{anyhow, bail, Context};
use niri_ipc::{LogicalOutput, Mode, Output, Reply, Request, Response};
use crate::cli::Msg;
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
format!(
"{} is not set, are you running this within niri?",
niri_ipc::SOCKET_PATH_ENV
)
})?;
let mut stream =
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
let request = match &msg {
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::Action { action } => Request::Action(action.clone()),
};
let mut buf = serde_json::to_vec(&request).unwrap();
stream
.write_all(&buf)
.context("error writing IPC request")?;
stream
.shutdown(Shutdown::Write)
.context("error closing IPC stream for writing")?;
buf.clear();
stream
.read_to_end(&mut buf)
.context("error reading IPC response")?;
let reply: Reply = serde_json::from_slice(&buf).context("error parsing IPC reply")?;
let response = reply
.map_err(|msg| anyhow!(msg))
.context("niri could not handle the request")?;
match msg {
Msg::Outputs => {
let Response::Outputs(outputs) = response else {
bail!("unexpected response: expected Outputs, got {response:?}");
};
if json {
let output =
serde_json::to_string(&outputs).context("error formatting response")?;
println!("{output}");
return Ok(());
}
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
for (connector, output) in outputs.into_iter() {
let Output {
name,
make,
model,
physical_size,
modes,
current_mode,
logical,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
if let Some(current) = current_mode {
let mode = *modes
.get(current)
.context("invalid response: current mode does not exist")?;
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
let preferred = if is_preferred { " (preferred)" } else { "" };
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
} else {
println!(" Disabled");
}
if let Some((width, height)) = physical_size {
println!(" Physical size: {width}x{height} mm");
} else {
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 (idx, mode) in modes.into_iter().enumerate() {
let Mode {
width,
height,
refresh_rate,
is_preferred,
} = mode;
let refresh = refresh_rate as f64 / 1000.;
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:?}");
};
}
}
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod client;
pub mod server;
+166
View File
@@ -0,0 +1,166 @@
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::{env, io, process};
use anyhow::Context;
use calloop::io::Async;
use directories::BaseDirs;
use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
use niri_ipc::{Request, Response};
use smithay::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 {
pub socket_path: PathBuf,
}
struct ClientCtx {
event_loop: LoopHandle<'static, State>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
ipc_focused_window: Arc<Mutex<Option<Window>>>,
}
impl IpcServer {
pub fn start(
event_loop: &LoopHandle<'static, State>,
wayland_socket_name: &str,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("Ipc::start");
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
let mut socket_path = socket_dir();
socket_path.push(socket_name);
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
listener
.set_nonblocking(true)
.context("error setting socket to non-blocking")?;
let source = Generic::new(listener, Interest::READ, Mode::Level);
event_loop
.insert_source(source, |_, socket, state| {
match socket.accept() {
Ok((stream, _)) => on_new_ipc_client(state, stream),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
Ok(PostAction::Continue)
})
.unwrap();
Ok(Self { socket_path })
}
}
impl Drop for IpcServer {
fn drop(&mut self) {
let _ = unlink(&self.socket_path);
}
}
fn socket_dir() -> PathBuf {
BaseDirs::new()
.as_ref()
.and_then(|x| x.runtime_dir())
.map(|x| x.to_owned())
.unwrap_or_else(env::temp_dir)
}
fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
let _span = tracy_client::span!("on_new_ipc_client");
trace!("new IPC client connected");
let stream = match state.niri.event_loop.adapt_io(stream) {
Ok(stream) => stream,
Err(err) => {
warn!("error making IPC stream async: {err:?}");
return;
}
};
let ctx = ClientCtx {
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 {
if let Err(err) = handle_client(ctx, stream).await {
warn!("error handling IPC client: {err:?}");
}
};
if let Err(err) = state.niri.scheduler.schedule(future) {
warn!("error scheduling IPC stream future: {err:?}");
}
}
async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> {
let (read, mut write) = stream.split();
let mut buf = String::new();
// Read a single line to allow extensibility in the future to keep reading.
BufReader::new(read)
.read_line(&mut buf)
.await
.context("error reading request")?;
let reply = process(&ctx, &buf).map_err(|err| {
warn!("error processing IPC request: {err:?}");
err.to_string()
});
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
write.write_all(&buf).await.context("error writing reply")?;
Ok(())
}
fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
let request: Request = serde_json::from_str(buf).context("error parsing request")?;
let response = match request {
Request::Outputs => {
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| {
state.do_action(action);
});
Response::Handled
}
};
Ok(response)
}
-3870
View File
File diff suppressed because it is too large Load Diff
+164
View File
@@ -0,0 +1,164 @@
use std::iter::zip;
use arrayvec::ArrayVec;
use niri_config::GradientRelativeTo;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use crate::niri_render_elements;
use crate::render_helpers::gradient::GradientRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
sizes: [Size<i32, Logical>; 4],
full_size: Size<i32, Logical>,
is_active: bool,
is_border: bool,
config: niri_config::FocusRing,
}
niri_render_elements! {
FocusRingRenderElement => {
SolidColor = SolidColorRenderElement,
Gradient = GradientRenderElement,
}
}
impl FocusRing {
pub fn new(config: niri_config::FocusRing) -> Self {
Self {
buffers: Default::default(),
locations: Default::default(),
sizes: Default::default(),
full_size: Default::default(),
is_active: false,
is_border: false,
config,
}
}
pub fn update_config(&mut self, config: niri_config::FocusRing) {
self.config = config;
}
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
let width = i32::from(self.config.width);
self.full_size = win_size + Size::from((width * 2, width * 2));
if is_border {
self.sizes[0] = Size::from((win_size.w + width * 2, width));
self.sizes[1] = Size::from((win_size.w + width * 2, width));
self.sizes[2] = Size::from((width, win_size.h));
self.sizes[3] = Size::from((width, win_size.h));
for (buf, size) in zip(&mut self.buffers, self.sizes) {
buf.resize(size);
}
self.locations[0] = Point::from((-width, -width));
self.locations[1] = Point::from((-width, win_size.h));
self.locations[2] = Point::from((-width, 0));
self.locations[3] = Point::from((win_size.w, 0));
} else {
self.sizes[0] = self.full_size;
self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
}
self.is_border = is_border;
}
pub fn set_active(&mut self, is_active: bool) {
let color = if is_active {
self.config.active_color.into()
} else {
self.config.inactive_color.into()
};
for buf in &mut self.buffers {
buf.set_color(color);
}
self.is_active = is_active;
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
view_size: Size<i32, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
if self.config.off {
return rv.into_iter();
}
let gradient = if self.is_active {
self.config.active_gradient
} else {
self.config.inactive_gradient
};
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
let elem = gradient.and_then(|gradient| {
let gradient_area = match gradient.relative_to {
GradientRelativeTo::Window => full_rect,
GradientRelativeTo::WorkspaceView => view_rect,
};
GradientRenderElement::new(
renderer,
scale,
Rectangle::from_loc_and_size(location, size),
gradient_area,
gradient.from.into(),
gradient.to.into(),
((gradient.angle as f32) - 90.).to_radians(),
)
.map(Into::into)
});
let elem = elem.unwrap_or_else(|| {
SolidColorRenderElement::from_buffer(
buffer,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
});
rv.push(elem);
};
if self.is_border {
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
push(buf, location + loc, size);
}
} else {
push(
&self.buffers[0],
location + self.locations[0],
self.sizes[0],
);
}
rv.into_iter()
}
pub fn width(&self) -> i32 {
self.config.width.into()
}
pub fn is_off(&self) -> bool {
self.config.off
}
}
+3028
View File
File diff suppressed because it is too large Load Diff
+885
View File
@@ -0,0 +1,885 @@
use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use super::workspace::{
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;
/// Amount of touchpad movement to scroll the height of one workspace.
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
stiffness: 0.5,
limit: 0.05,
};
#[derive(Debug)]
pub struct Monitor<W: LayoutElement> {
/// Output for this monitor.
pub output: Output,
// Must always contain at least one.
pub workspaces: Vec<Workspace<W>>,
/// Index of the currently active workspace.
pub active_workspace_idx: usize,
/// 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.
pub options: Rc<Options>,
}
#[derive(Debug)]
pub enum WorkspaceSwitch {
Animation(Animation),
Gesture(WorkspaceSwitchGesture),
}
#[derive(Debug)]
pub struct WorkspaceSwitchGesture {
/// Index of the workspace where the gesture was started.
pub center_idx: usize,
/// Current, fractional workspace index.
pub current_idx: f64,
pub tracker: SwipeTracker,
}
pub type MonitorRenderElement<R> =
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
impl WorkspaceSwitch {
pub fn current_idx(&self) -> f64 {
match self {
WorkspaceSwitch::Animation(anim) => anim.value(),
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
}
}
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
#[must_use]
fn is_animation(&self) -> bool {
matches!(self, Self::Animation(..))
}
}
impl<W: LayoutElement> Monitor<W> {
pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
Self {
output,
workspaces,
active_workspace_idx: 0,
previous_workspace_id: None,
workspace_switch: None,
options,
}
}
pub fn active_workspace_ref(&self) -> &Workspace<W> {
&self.workspaces[self.active_workspace_idx]
}
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
&mut self.workspaces[self.active_workspace_idx]
}
fn activate_workspace(&mut self, idx: usize) {
if self.active_workspace_idx == idx {
return;
}
// FIXME: also compute and use current velocity.
let current_idx = self
.workspace_switch
.as_ref()
.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(
current_idx,
idx as f64,
0.,
self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(),
)));
}
pub fn add_window(
&mut self,
workspace_idx: usize,
window: W,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window(window, activate, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn add_window_right_of(
&mut self,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace_idx = self
.workspaces
.iter_mut()
.position(|ws| ws.has_window(right_of))
.unwrap();
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_window_right_of(right_of, window, width, is_full_width);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
}
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_column(column, activate);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn clean_up_workspaces(&mut self) {
assert!(self.workspace_switch.is_none());
for idx in (0..self.workspaces.len() - 1).rev() {
if self.active_workspace_idx == idx {
continue;
}
if !self.workspaces[idx].has_windows() {
self.workspaces.remove(idx);
if self.active_workspace_idx > idx {
self.active_workspace_idx -= 1;
}
}
}
}
pub fn move_left(&mut self) {
self.active_workspace().move_left();
}
pub fn move_right(&mut self) {
self.active_workspace().move_right();
}
pub fn move_column_to_first(&mut self) {
self.active_workspace().move_column_to_first();
}
pub fn move_column_to_last(&mut self) {
self.active_workspace().move_column_to_last();
}
pub fn move_down(&mut self) {
self.active_workspace().move_down();
}
pub fn move_up(&mut self) {
self.active_workspace().move_up();
}
pub fn move_down_or_to_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let column = &mut workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.move_to_workspace_down();
} else {
workspace.move_down();
}
}
pub fn move_up_or_to_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.move_to_workspace_up();
} else {
workspace.move_up();
}
}
pub fn consume_or_expel_window_left(&mut self) {
self.active_workspace().consume_or_expel_window_left();
}
pub fn consume_or_expel_window_right(&mut self) {
self.active_workspace().consume_or_expel_window_right();
}
pub fn focus_left(&mut self) {
self.active_workspace().focus_left();
}
pub fn focus_right(&mut self) {
self.active_workspace().focus_right();
}
pub fn focus_column_first(&mut self) {
self.active_workspace().focus_column_first();
}
pub fn focus_column_last(&mut self) {
self.active_workspace().focus_column_last();
}
pub fn focus_down(&mut self) {
self.active_workspace().focus_down();
}
pub fn focus_up(&mut self) {
self.active_workspace().focus_up();
}
pub fn focus_window_or_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_down();
} else {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_tile_idx;
let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1);
if curr_idx == new_idx {
self.switch_workspace_down();
} else {
workspace.focus_down();
}
}
}
pub fn focus_window_or_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.switch_workspace_up();
} else {
workspace.focus_up();
}
}
}
pub fn move_to_workspace_up(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = source_workspace_idx.saturating_sub(1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
self.add_window(new_idx, window, true, width, is_full_width);
}
pub fn move_to_workspace_down(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
self.add_window(new_idx, window, true, width, is_full_width);
}
pub fn move_to_workspace(&mut self, idx: usize) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = &workspace.columns[workspace.active_column_idx];
let width = column.width;
let is_full_width = column.is_full_width;
let window =
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
self.add_window(new_idx, window, true, width, is_full_width);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn move_column_to_workspace_up(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = source_workspace_idx.saturating_sub(1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
}
pub fn move_column_to_workspace_down(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
}
pub fn move_column_to_workspace(&mut self, idx: usize) {
let source_workspace_idx = self.active_workspace_idx;
let new_idx = min(idx, self.workspaces.len() - 1);
if new_idx == source_workspace_idx {
return;
}
let workspace = &mut self.workspaces[source_workspace_idx];
if workspace.columns.is_empty() {
return;
}
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
self.add_column(new_idx, column, true);
// Don't animate this action.
self.workspace_switch = None;
self.clean_up_workspaces();
}
pub fn switch_workspace_up(&mut self) {
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
}
pub fn switch_workspace_down(&mut self) {
self.activate_workspace(min(
self.active_workspace_idx + 1,
self.workspaces.len() - 1,
));
}
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.
self.workspace_switch = None;
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();
}
pub fn expel_from_column(&mut self) {
self.active_workspace().expel_from_column();
}
pub fn center_column(&mut self) {
self.active_workspace().center_column();
}
pub fn focus(&self) -> Option<&W> {
let workspace = &self.workspaces[self.active_workspace_idx];
if !workspace.has_windows() {
return None;
}
let column = &workspace.columns[workspace.active_column_idx];
Some(column.tiles[column.active_tile_idx].window())
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
anim.set_current_time(current_time);
if anim.is_done() {
self.workspace_switch = None;
self.clean_up_workspaces();
}
}
for ws in &mut self.workspaces {
ws.advance_animations(current_time, is_active);
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.workspace_switch
.as_ref()
.is_some_and(|s| s.is_animation())
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
}
pub fn are_transitions_ongoing(&self) -> bool {
self.workspace_switch.is_some()
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
}
pub fn update_config(&mut self, options: Rc<Options>) {
for ws in &mut self.workspaces {
ws.update_config(options.clone());
}
if self.options.struts != options.struts {
let view_size = output_size(&self.output);
let working_area = compute_working_area(&self.output, options.struts);
for ws in &mut self.workspaces {
ws.set_view_size(view_size, working_area);
}
}
self.options = options;
}
pub fn toggle_width(&mut self) {
self.active_workspace().toggle_width();
}
pub fn toggle_full_width(&mut self) {
self.active_workspace().toggle_full_width();
}
pub fn set_column_width(&mut self, change: SizeChange) {
self.active_workspace().set_column_width(change);
}
pub fn set_window_height(&mut self, change: SizeChange) {
self.active_workspace().set_window_height(change);
}
pub fn move_workspace_down(&mut self) {
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
if new_idx == self.active_workspace_idx {
return;
}
self.workspaces.swap(self.active_workspace_idx, new_idx);
if new_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
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();
}
pub fn move_workspace_up(&mut self) {
let new_idx = self.active_workspace_idx.saturating_sub(1);
if new_idx == self.active_workspace_idx {
return;
}
self.workspaces.swap(self.active_workspace_idx, new_idx);
if self.active_workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
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>,
) -> Option<(&W, Option<Point<i32, Logical>>)> {
match &self.workspace_switch {
Some(switch) => {
let size = output_size(&self.output);
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return None;
}
let after_idx = after_idx as usize;
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
if before_idx < 0. {
return None;
}
(before_idx as usize, Point::from((0, offset)))
} else {
if after_idx >= self.workspaces.len() {
return None;
}
(after_idx, Point::from((0, -size.h + offset)))
};
let ws = &self.workspaces[idx];
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset.to_f64())?;
Some((win, win_pos.map(|p| p - ws_offset)))
}
None => {
let ws = &self.workspaces[self.active_workspace_idx];
ws.window_under(pos_within_output)
}
}
}
pub fn render_above_top_layer(&self) -> bool {
// Render above the top layer only if the view is stationary.
if self.workspace_switch.is_some() {
return false;
}
let ws = &self.workspaces[self.active_workspace_idx];
ws.render_above_top_layer()
}
pub fn render_elements<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
) -> Vec<MonitorRenderElement<R>> {
let _span = tracy_client::span!("Monitor::render_elements");
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
let output_transform = self.output.current_transform();
let output_mode = self.output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
match &self.workspace_switch {
Some(switch) => {
let render_idx = switch.current_idx();
let before_idx = render_idx.floor();
let after_idx = render_idx.ceil();
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
return vec![];
}
let after_idx = after_idx as usize;
let after = if after_idx < self.workspaces.len() {
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(
elem,
output_scale,
// HACK: crop to infinite bounds for all sides except the side
// where the workspaces join,
// otherwise it will cut pixel shaders and mess up
// the coordinate space.
Rectangle::from_extemities(
(-i32::MAX / 2, 0),
(i32::MAX / 2, i32::MAX / 2),
),
)?,
(0, -offset + size.h),
Relocate::Relative,
))
});
if before_idx < 0. {
return after.collect();
}
Some(after)
} else {
None
};
let before_idx = before_idx as usize;
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(
elem,
output_scale,
Rectangle::from_extemities(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX / 2, size.h),
),
)?,
(0, -offset),
Relocate::Relative,
))
});
before.chain(after.into_iter().flatten()).collect()
}
None => {
let elements =
self.workspaces[self.active_workspace_idx].render_elements(renderer, target);
elements
.into_iter()
.filter_map(|elem| {
Some(RelocateRenderElement::from_element(
CropRenderElement::from_element(
elem,
output_scale,
// HACK: set infinite crop bounds due to a damage tracking bug
// which causes glitched rendering for maximized GTK windows.
// FIXME: use proper bounds after fixing the Crop element.
Rectangle::from_loc_and_size(
(-i32::MAX / 2, -i32::MAX / 2),
(i32::MAX, i32::MAX),
),
// Rectangle::from_loc_and_size((0, 0), size),
)?,
(0, 0),
Relocate::Relative,
))
})
.collect()
}
}
}
pub fn workspace_switch_gesture_begin(&mut self) {
let center_idx = self.active_workspace_idx;
let current_idx = self
.workspace_switch
.as_ref()
.map(|s| s.current_idx())
.unwrap_or(center_idx as f64);
let gesture = WorkspaceSwitchGesture {
center_idx,
current_idx,
tracker: SwipeTracker::new(),
};
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
}
pub fn workspace_switch_gesture_update(
&mut self,
delta_y: f64,
timestamp: Duration,
) -> Option<bool> {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return None;
};
gesture.tracker.push(delta_y, timestamp);
let pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
let new_idx = gesture.center_idx as f64 + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
if gesture.current_idx == new_idx {
return Some(false);
}
gesture.current_idx = new_idx;
Some(true)
}
pub fn workspace_switch_gesture_end(&mut self, cancelled: bool) -> bool {
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
return false;
};
if cancelled {
self.workspace_switch = None;
self.clean_up_workspaces();
return true;
}
let mut velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
let current_pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
let min = gesture.center_idx.saturating_sub(1) as f64;
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
let new_idx = gesture.center_idx as f64 + pos;
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
let new_idx = new_idx.round() as usize;
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
min,
max,
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,
new_idx as f64,
velocity,
self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(),
)));
true
}
}
+419
View File
@@ -0,0 +1,419 @@
use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::{LayoutElement, LayoutElementRenderElement, Options};
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)]
pub struct Tile<W: LayoutElement> {
/// The toplevel window itself.
window: W,
/// The border around the window.
border: FocusRing,
/// The focus ring around the window.
///
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
/// currently here.
focus_ring: FocusRing,
/// Whether this tile is fullscreen.
///
/// This will update only when the `window` actually goes fullscreen, rather than right away,
/// to avoid black backdrop flicker before the window has had a chance to resize.
is_fullscreen: bool,
/// The black backdrop for fullscreen windows.
fullscreen_backdrop: SolidColorBuffer,
/// The size we were requested to fullscreen into.
fullscreen_size: Size<i32, Logical>,
/// The animation upon opening a window.
open_animation: Option<Animation>,
/// Configurable properties of the layout.
options: Rc<Options>,
}
niri_render_elements! {
TileRenderElement<R> => {
LayoutElement = LayoutElementRenderElement<R>,
FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
}
}
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, options: Rc<Options>) -> Self {
Self {
window,
border: FocusRing::new(options.border.into()),
focus_ring: FocusRing::new(options.focus_ring),
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
fullscreen_size: Default::default(),
open_animation: None,
options,
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
self.border.update_config(options.border.into());
self.focus_ring.update_config(options.focus_ring);
self.options = options;
}
pub fn update_window(&mut self) {
// FIXME: remove when we can get a fullscreen size right away.
if self.fullscreen_size != Size::from((0, 0)) {
self.is_fullscreen = self.window.is_fullscreen();
}
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
let draw_border_with_background = self
.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
self.border
.update(self.window.size(), !draw_border_with_background);
self.border.set_active(is_active);
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 {
Some(anim) => {
anim.set_current_time(current_time);
if anim.is_done() {
self.open_animation = None;
}
}
None => (),
}
}
pub fn are_animations_ongoing(&self) -> bool {
self.open_animation.is_some()
}
pub fn start_open_animation(&mut self) {
self.open_animation = Some(Animation::new(
0.,
1.,
0.,
self.options.animations.window_open,
niri_config::Animation::default_window_open(),
));
}
pub fn window(&self) -> &W {
&self.window
}
pub fn window_mut(&mut self) -> &mut W {
&mut self.window
}
pub fn into_window(self) -> W {
self.window
}
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
fn effective_border_width(&self) -> Option<i32> {
if self.is_fullscreen {
return None;
}
if self.border.is_off() {
return None;
}
Some(self.border.width())
}
/// Returns the location of the window's visual geometry within this Tile.
pub fn window_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
// In fullscreen, center the window in the given size.
if self.is_fullscreen {
let window_size = self.window.size();
let target_size = self.fullscreen_size;
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
// one, leave it at the top-left as usual.
if window_size.w < target_size.w {
loc.x += (target_size.w - window_size.w) / 2;
}
if window_size.h < target_size.h {
loc.y += (target_size.h - window_size.h) / 2;
}
}
if let Some(width) = self.effective_border_width() {
loc += (width, width).into();
}
loc
}
pub fn tile_size(&self) -> Size<i32, Logical> {
let mut size = self.window.size();
if self.is_fullscreen {
// Normally we'd just return the fullscreen size here, but this makes things a bit
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
size.w = max(size.w, self.fullscreen_size.w);
size.h = max(size.h, self.fullscreen_size.h);
return size;
}
if let Some(width) = self.effective_border_width() {
size.w = size.w.saturating_add(width * 2);
size.h = size.h.saturating_add(width * 2);
}
size
}
pub fn window_size(&self) -> Size<i32, Logical> {
self.window.size()
}
/// Returns an animated size of the tile for rendering and input.
///
/// During the window opening animation, windows to the right should gradually slide further to
/// the right. This is what this visual size is used for. Other things like window resizes or
/// transactions or new view position calculation always use the real size, instead of this
/// visual size.
pub fn visual_tile_size(&self) -> Size<i32, Logical> {
let size = self.tile_size();
let v = self
.open_animation
.as_ref()
.map(|anim| anim.value())
.unwrap_or(1.)
.max(0.);
Size::from(((f64::from(size.w) * v).round() as i32, size.h))
}
pub fn buf_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
loc += self.window_loc();
loc += self.window.buf_loc();
loc
}
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
point -= self.window_loc().to_f64();
self.window.is_in_input_region(point)
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
let activation_region = Rectangle::from_loc_and_size((0, 0), self.tile_size());
activation_region.to_f64().contains(point)
}
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>) {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
size.w = max(1, size.w - width * 2);
size.h = max(1, size.h - width * 2);
}
self.window.request_size(size);
}
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
if self.border.is_off() {
size
} else {
size.saturating_add(self.border.width() * 2)
}
}
pub fn tile_height_for_window_height(&self, size: i32) -> i32 {
if self.border.is_off() {
size
} else {
size.saturating_add(self.border.width() * 2)
}
}
pub fn window_height_for_tile_height(&self, size: i32) -> i32 {
if self.border.is_off() {
size
} else {
size.saturating_sub(self.border.width() * 2)
}
}
pub fn request_fullscreen(&mut self, size: Size<i32, Logical>) {
self.fullscreen_backdrop.resize(size);
self.fullscreen_size = size;
self.window.request_fullscreen(size);
}
pub fn min_size(&self) -> Size<i32, Logical> {
let mut size = self.window.min_size();
if let Some(width) = self.effective_border_width() {
size.w = max(1, size.w);
size.h = max(1, size.h);
size.w = size.w.saturating_add(width * 2);
size.h = size.h.saturating_add(width * 2);
}
size
}
pub fn max_size(&self) -> Size<i32, Logical> {
let mut size = self.window.max_size();
if let Some(width) = self.effective_border_width() {
if size.w > 0 {
size.w = size.w.saturating_add(width * 2);
}
if size.h > 0 {
size.h = size.h.saturating_add(width * 2);
}
}
size
}
pub fn 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>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
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, alpha, target)
.into_iter()
.map(Into::into);
let elem = self.effective_border_width().map(|width| {
self.border
.render(
renderer,
location + Point::from((width, width)),
scale,
view_size,
)
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
let elem = focus_ring.then(|| {
self.focus_ring
.render(renderer, location, scale, view_size)
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
let elem = self.is_fullscreen.then(|| {
SolidColorRenderElement::from_buffer(
&self.fullscreen_backdrop,
location.to_physical_precise_round(scale),
scale,
1.,
Kind::Unspecified,
)
.into()
});
rv.chain(elem)
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
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, target);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
let elem = OffscreenRenderElement::new(
renderer,
scale.x as i32,
&elements,
anim.value().clamp(0., 1.) as f32,
);
self.window()
.set_offscreen_element_id(Some(elem.id().clone()));
let mut center = location;
center.x += self.tile_size().w / 2;
center.y += self.tile_size().h / 2;
Some(TileRenderElement::Offscreen(
RescaleRenderElement::from_element(
elem,
center.to_physical_precise_round(scale),
(anim.value() / 2. + 0.5).max(0.),
),
))
.into_iter()
.chain(None.into_iter().flatten())
} else {
self.window().set_offscreen_element_id(None);
let elements =
self.render_inner(renderer, location, scale, view_size, focus_ring, target);
None.into_iter().chain(Some(elements).into_iter().flatten())
}
}
}
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
#[macro_use]
extern crate tracing;
pub mod animation;
pub mod backend;
pub mod cli;
pub mod cursor;
#[cfg(feature = "dbus")]
pub mod dbus;
pub mod frame_clock;
pub mod handlers;
pub mod input;
pub mod ipc;
pub mod layout;
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;
pub mod window;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
pub mod pw_utils;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub use dummy_pw_utils as pw_utils;
+203 -78
View File
@@ -1,60 +1,35 @@
#[macro_use]
extern crate tracing;
mod animation;
mod backend;
mod config;
mod cursor;
#[cfg(feature = "dbus")]
mod dbus;
mod frame_clock;
mod handlers;
mod input;
mod layout;
mod niri;
mod screenshot_ui;
mod utils;
mod watcher;
#[cfg(not(feature = "xdp-gnome-screencast"))]
mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
mod pw_utils;
use std::ffi::OsString;
use std::fmt::Write as _;
use std::fs::{self, File};
use std::io::{self, Write};
use std::os::fd::FromRawFd;
use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use clap::Parser;
use config::Config;
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
use git_version::git_version;
use miette::{Context, NarratableReportHandler};
use niri::{Niri, State};
use directories::ProjectDirs;
use niri::animation;
use niri::cli::{Cli, Sub};
#[cfg(feature = "dbus")]
use niri::dbus;
use niri::ipc::client::handle_msg;
use niri::niri::State;
use niri::utils::spawning::{
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
};
use niri::utils::watcher::Watcher;
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
use niri_config::Config;
use portable_atomic::Ordering;
use sd_notify::NotifyState;
use smithay::reexports::calloop::{self, EventLoop};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::Display;
use tracing_subscriber::EnvFilter;
use utils::spawn;
use watcher::Watcher;
use crate::utils::{REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
/// Command to run upon compositor startup.
#[arg(last = true)]
command: Vec<OsString>,
}
fn main() {
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
if env::var_os("RUST_BACKTRACE").is_none() {
env::set_var("RUST_BACKTRACE", "1");
@@ -65,7 +40,15 @@ fn main() {
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
}
let is_systemd_service = env::var_os("NOTIFY_SOCKET").is_some();
if env::var_os("NOTIFY_SOCKET").is_some() {
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
#[cfg(not(feature = "systemd"))]
warn!(
"running as a systemd service, but systemd support is compiled out. \
Are you sure you did not forget to set `--features systemd`?"
);
}
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
let env_filter = EnvFilter::builder().parse_lossy(directives);
@@ -74,41 +57,115 @@ fn main() {
.with_env_filter(env_filter)
.init();
if is_systemd_service {
// If we're starting as a systemd service, assume that the intention is to start on a TTY.
// Remove DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will
// cause the winit backend to be selected instead.
let cli = Cli::parse();
if cli.session {
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
// the winit backend to be selected instead.
if env::var_os("DISPLAY").is_some() {
debug!("we're running as a systemd service but DISPLAY is set, removing it");
warn!("running as a session but DISPLAY is set, removing it");
env::remove_var("DISPLAY");
}
if env::var_os("WAYLAND_DISPLAY").is_some() {
debug!("we're running as a systemd service but WAYLAND_DISPLAY is set, removing it");
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
env::remove_var("WAYLAND_DISPLAY");
}
}
let cli = Cli::parse();
// Set the current desktop for xdg-desktop-portal.
env::set_var("XDG_CURRENT_DESKTOP", "niri");
// Ensure the session type is set to Wayland for xdg-autostart and Qt apps.
env::set_var("XDG_SESSION_TYPE", "wayland");
}
let _client = tracy_client::Client::start();
info!(
"starting version {} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
);
// Set a better error printer for config loading.
niri_config::set_miette_hook().unwrap();
// Handle subcommands.
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
let path = config
.or_else(default_config_path)
.expect("error getting config path");
Config::load(&path)?;
info!("config is valid");
return Ok(());
}
Sub::Msg { msg, json } => {
handle_msg(msg, json)?;
return Ok(());
}
Sub::Panic => cause_panic(),
}
}
info!("starting version {}", &version());
// Load the config.
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))).unwrap();
let (mut config, path) = match Config::load(cli.config).context("error loading config") {
Ok((config, path)) => (config, Some(path)),
Err(err) => {
warn!("{err:?}");
(Config::default(), None)
let mut config_created = false;
let path = cli.config.or_else(|| {
let default_path = default_config_path()?;
let default_parent = default_path.parent().unwrap();
if let Err(err) = fs::create_dir_all(default_parent) {
warn!(
"error creating config directories {:?}: {err:?}",
default_parent
);
return Some(default_path);
}
// Create the config and fill it with the default config if it doesn't exist.
let new_file = File::options()
.read(true)
.write(true)
.create_new(true)
.open(&default_path);
match new_file {
Ok(mut new_file) => {
let default = include_bytes!("../resources/default-config.kdl");
match new_file.write_all(default) {
Ok(()) => {
config_created = true;
info!("wrote default config to {:?}", &default_path);
}
Err(err) => {
warn!("error writing config file at {:?}: {err:?}", &default_path)
}
}
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
}
Some(default_path)
});
let mut config_errored = false;
let mut config = path
.as_deref()
.and_then(|path| match Config::load(path) {
Ok(config) => Some(config),
Err(err) => {
warn!("{err:?}");
config_errored = true;
None
}
})
.unwrap_or_default();
let slowdown = if config.animations.off {
0.
} else {
config.animations.slowdown.clamp(0., 100.)
};
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
// Create the compositor.
let mut event_loop = EventLoop::try_new().unwrap();
@@ -118,7 +175,8 @@ fn main() {
event_loop.handle(),
event_loop.get_signal(),
display,
);
)
.unwrap();
// Set WAYLAND_DISPLAY for children.
let socket_name = &state.niri.socket_name;
@@ -128,27 +186,40 @@ fn main() {
socket_name.to_string_lossy()
);
if is_systemd_service {
// We're starting as a systemd service. Export our variables.
import_env_to_systemd();
// Set NIRI_SOCKET for children.
if let Some(ipc) = &state.niri.ipc_server {
env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path);
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
}
if cli.session {
// We're starting as a session. Import our variables.
import_environment();
// Inhibit power key handling so we can suspend on it.
#[cfg(feature = "dbus")]
if let Err(err) = state.niri.inhibit_power_key() {
warn!("error inhibiting power key: {err:?}");
if !state.niri.config.borrow().input.disable_power_key_handling {
if let Err(err) = state.niri.inhibit_power_key() {
warn!("error inhibiting power key: {err:?}");
}
}
}
#[cfg(feature = "dbus")]
dbus::DBusServers::start(&mut state, is_systemd_service);
dbus::DBusServers::start(&mut state, cli.session);
// Notify systemd we're ready.
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
warn!("error notifying systemd: {err:?}");
};
// Send ready notification to the NOTIFY_FD file descriptor.
if let Err(err) = notify_fd() {
warn!("error notifying fd: {err:?}");
}
// Set up config file watcher.
let _watcher = if let Some(path) = path {
let _watcher = if let Some(path) = path.clone() {
let (tx, rx) = calloop::channel::sync_channel(1);
let watcher = Watcher::new(path.clone(), tx);
event_loop
@@ -170,19 +241,50 @@ fn main() {
spawn(elem.command);
}
// Show the config error notification right away if needed.
if config_errored {
state.niri.config_error_notification.show();
} else if config_created {
state.niri.config_error_notification.show_created(path);
}
// Run the compositor.
event_loop
.run(None, &mut state, |state| state.refresh_and_flush_clients())
.unwrap();
Ok(())
}
fn import_env_to_systemd() {
fn import_environment() {
let variables = [
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
niri_ipc::SOCKET_PATH_ENV,
]
.join(" ");
let mut init_system_import = String::new();
if cfg!(feature = "systemd") {
write!(
init_system_import,
"systemctl --user import-environment {variables};"
)
.unwrap();
}
if cfg!(feature = "dinit") {
write!(init_system_import, "dinitctl setenv {variables};").unwrap();
}
let rv = Command::new("/bin/sh")
.args([
"-c",
"systemctl --user import-environment WAYLAND_DISPLAY && \
hash dbus-update-activation-environment 2>/dev/null && \
dbus-update-activation-environment WAYLAND_DISPLAY",
&format!(
"{init_system_import}\
hash dbus-update-activation-environment 2>/dev/null && \
dbus-update-activation-environment {variables}"
),
])
.spawn();
// Wait for the import process to complete, otherwise services will start too fast without
@@ -199,7 +301,30 @@ fn import_env_to_systemd() {
}
},
Err(err) => {
warn!("error spawning shell to import environment into systemd: {err:?}");
warn!("error spawning shell to import environment: {err:?}");
}
}
}
fn default_config_path() -> Option<PathBuf> {
let Some(dirs) = ProjectDirs::from("", "", "niri") else {
warn!("error retrieving home directory");
return None;
};
let mut path = dirs.config_dir().to_owned();
path.push("config.kdl");
Some(path)
}
fn notify_fd() -> anyhow::Result<()> {
let fd = match env::var("NOTIFY_FD") {
Ok(notify_fd) => notify_fd.parse()?,
Err(env::VarError::NotPresent) => return Ok(()),
Err(err) => return Err(err.into()),
};
env::remove_var("NOTIFY_FD");
let mut notif = unsafe { File::from_raw_fd(fd) };
notif.write_all(b"READY=1\n")?;
Ok(())
}
+1728 -558
View File
File diff suppressed because it is too large Load Diff
+466
View File
@@ -0,0 +1,466 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use arrayvec::ArrayVec;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
};
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
const VERSION: u32 = 3;
pub struct ForeignToplevelManagerState {
display: DisplayHandle,
instances: Vec<ZwlrForeignToplevelManagerV1>,
toplevels: HashMap<WlSurface, ToplevelData>,
}
pub trait ForeignToplevelHandler {
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
fn activate(&mut self, wl_surface: WlSurface);
fn close(&mut self, wl_surface: WlSurface);
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
fn unset_fullscreen(&mut self, wl_surface: WlSurface);
}
struct ToplevelData {
title: Option<String>,
app_id: Option<String>,
states: ArrayVec<u32, 3>,
output: Option<Output>,
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
// FIXME: parent.
}
pub struct ForeignToplevelGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ForeignToplevelManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ForeignToplevelGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
Self {
display: display.clone(),
instances: Vec::new(),
toplevels: HashMap::new(),
}
}
}
pub fn refresh(state: &mut State) {
let _span = tracy_client::span!("foreign_toplevel::refresh");
let protocol_state = &mut state.niri.foreign_toplevel_state;
// Handle closed windows.
protocol_state.toplevels.retain(|surface, data| {
if state.niri.layout.find_window_and_output(surface).is_some() {
return true;
}
for instance in data.instances.keys() {
instance.closed();
}
false
});
// Handle new and existing windows.
//
// Save the focused window for last, this way when the focus changes, we will first deactivate
// the previous window and only then activate the newly focused window.
let mut focused = None;
state.niri.layout.with_windows(|mapped, output| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
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);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
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();
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
});
}
}
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
let Some(client) = wl_output.client() else {
return;
};
let protocol_state = &mut state.niri.foreign_toplevel_state;
for data in protocol_state.toplevels.values_mut() {
if data.output.as_ref() != Some(output) {
continue;
}
for (instance, outputs) in &mut data.instances {
if instance.client().as_ref() != Some(&client) {
continue;
}
instance.output_enter(wl_output);
instance.done();
outputs.push(wl_output.clone());
}
}
}
fn refresh_toplevel(
protocol_state: &mut ForeignToplevelManagerState,
wl_surface: &WlSurface,
role: &XdgToplevelSurfaceRoleAttributes,
output: Option<&Output>,
has_focus: bool,
) {
let states = to_state_vec(&role.current.states, has_focus);
match protocol_state.toplevels.entry(wl_surface.clone()) {
Entry::Occupied(entry) => {
// Existing window, check if anything changed.
let data = entry.into_mut();
let mut new_title = None;
if data.title != role.title {
data.title.clone_from(&role.title);
new_title = role.title.as_deref();
if new_title.is_none() {
error!("toplevel title changed to None");
}
}
let mut new_app_id = None;
if data.app_id != role.app_id {
data.app_id.clone_from(&role.app_id);
new_app_id = role.app_id.as_deref();
if new_app_id.is_none() {
error!("toplevel app_id changed to None");
}
}
let mut states_changed = false;
if data.states != states {
data.states = states;
states_changed = true;
}
let mut output_changed = false;
if data.output.as_ref() != output {
data.output = output.cloned();
output_changed = true;
}
let something_changed =
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
if something_changed {
for (instance, outputs) in &mut data.instances {
if let Some(new_title) = new_title {
instance.title(new_title.to_owned());
}
if let Some(new_app_id) = new_app_id {
instance.app_id(new_app_id.to_owned());
}
if states_changed {
instance.state(data.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
}
if output_changed {
for wl_output in outputs.drain(..) {
instance.output_leave(&wl_output);
}
if let Some(output) = &data.output {
if let Some(client) = instance.client() {
for wl_output in output.client_outputs(&client) {
instance.output_enter(&wl_output);
outputs.push(wl_output);
}
}
}
}
instance.done();
}
}
for outputs in data.instances.values_mut() {
// Clean up dead wl_outputs.
outputs.retain(|x| x.is_alive());
}
}
Entry::Vacant(entry) => {
// New window, start tracking it.
let mut data = ToplevelData {
title: role.title.clone(),
app_id: role.app_id.clone(),
states,
output: output.cloned(),
instances: HashMap::new(),
};
for manager in &protocol_state.instances {
if let Some(client) = manager.client() {
data.add_instance::<State>(&protocol_state.display, &client, manager);
}
}
entry.insert(data);
}
}
}
impl ToplevelData {
fn add_instance<D>(
&mut self,
handle: &DisplayHandle,
client: &Client,
manager: &ZwlrForeignToplevelManagerV1,
) where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: 'static,
{
let toplevel = client
.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
.unwrap();
manager.toplevel(&toplevel);
if let Some(title) = &self.title {
toplevel.title(title.clone());
}
if let Some(app_id) = &self.app_id {
toplevel.app_id(app_id.clone());
}
toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
let mut outputs = Vec::new();
if let Some(output) = &self.output {
for wl_output in output.client_outputs(client) {
toplevel.output_enter(&wl_output);
outputs.push(wl_output);
}
}
toplevel.done();
self.instances.insert(toplevel, outputs);
}
}
impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
for ForeignToplevelManagerState
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn bind(
state: &mut D,
handle: &DisplayHandle,
client: &Client,
resource: New<ZwlrForeignToplevelManagerV1>,
_global_data: &ForeignToplevelGlobalData,
data_init: &mut DataInit<'_, D>,
) {
let manager = data_init.init(resource, ());
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.add_instance::<D>(handle, client, &manager);
}
state.instances.push(manager);
}
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelManagerV1,
request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
resource.finished();
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelManagerV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
}
}
impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ZwlrForeignToplevelHandleV1,
request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
let protocol_state = state.foreign_toplevel_manager_state();
let Some((surface, _)) = protocol_state
.toplevels
.iter()
.find(|(_, data)| data.instances.contains_key(resource))
else {
return;
};
let surface = surface.clone();
match request {
zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
state.activate(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::Close => {
state.close(surface);
}
zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
state.set_fullscreen(surface, output);
}
zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
state.unset_fullscreen(surface);
}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ZwlrForeignToplevelHandleV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.instances.retain(|instance, _| instance != resource);
}
}
}
fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3> {
let mut rv = ArrayVec::new();
if states.contains(xdg_toplevel::State::Maximized) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
}
if states.contains(xdg_toplevel::State::Fullscreen) {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
}
// HACK: wlr-foreign-toplevel-management states:
//
// These have the same meaning as the states with the same names defined in xdg-toplevel
//
// However, clients such as sfwbar and fcitx seem to treat the activated state as keyboard
// focus, i.e. they don't expect multiple windows to have it set at once. Even Waybar which
// handles multiple activated windows correctly uses it in its design in such a way that
// keyboard focus would make more sense. Let's do what the clients expect.
if has_focus {
rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
}
rv
}
#[macro_export]
macro_rules! delegate_foreign_toplevel {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
};
}
+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);
};
}
+3
View File
@@ -0,0 +1,3 @@
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod screencopy;
+386
View File
@@ -0,0 +1,386 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use smithay::output::Output;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
Flags, ZwlrScreencopyFrameV1,
};
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::utils::{Physical, Point, Rectangle, Size};
use smithay::wayland::shm;
// We do not support copy_with_damage() semantics yet.
const VERSION: u32 = 1;
pub struct ScreencopyManagerState;
pub struct ScreencopyManagerGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ScreencopyManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ScreencopyManagerGlobalData {
filter: Box::new(filter),
};
display.create_global::<D, ZwlrScreencopyManagerV1, _>(VERSION, global_data);
Self
}
}
impl<D> GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData, D>
for ScreencopyManagerState
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn bind(
_state: &mut D,
_display: &DisplayHandle,
_client: &Client,
manager: New<ZwlrScreencopyManagerV1>,
_manager_state: &ScreencopyManagerGlobalData,
data_init: &mut DataInit<'_, D>,
) {
data_init.init(manager, ());
}
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ZwlrScreencopyManagerV1, (), D> for ScreencopyManagerState
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn request(
_state: &mut D,
_client: &Client,
_manager: &ZwlrScreencopyManagerV1,
request: zwlr_screencopy_manager_v1::Request,
_data: &(),
_display: &DisplayHandle,
data_init: &mut DataInit<'_, D>,
) {
let (frame, overlay_cursor, buffer_size, region_loc, output) = match request {
zwlr_screencopy_manager_v1::Request::CaptureOutput {
frame,
overlay_cursor,
output,
} => {
let output = Output::from_resource(&output).unwrap();
let buffer_size = output.current_mode().unwrap().size;
let region_loc = Point::from((0, 0));
(frame, overlay_cursor, buffer_size, region_loc, output)
}
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
frame,
overlay_cursor,
x,
y,
width,
height,
output,
} => {
if width <= 0 || height <= 0 {
trace!("screencopy client requested invalid sized region");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
}
let output = Output::from_resource(&output).unwrap();
let output_transform = output.current_transform();
let output_physical_size =
output_transform.transform_size(output.current_mode().unwrap().size);
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
let output_scale = output.current_scale().integer_scale();
let physical_rect = rect.to_physical(output_scale);
// Clamp captured region to the output.
let Some(clamped_rect) = physical_rect.intersection(output_rect) else {
trace!("screencopy client requested region outside of output");
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
frame.failed();
return;
};
let untransformed_rect = output_transform
.invert()
.transform_rect_in(clamped_rect, &output_physical_size);
(
frame,
overlay_cursor,
untransformed_rect.size,
clamped_rect.loc,
output,
)
}
zwlr_screencopy_manager_v1::Request::Destroy => return,
_ => unreachable!(),
};
// Create the frame.
let overlay_cursor = overlay_cursor != 0;
let info = ScreencopyFrameInfo {
output,
overlay_cursor,
buffer_size,
region_loc,
};
let frame = data_init.init(
frame,
ScreencopyFrameState::Pending {
info,
copied: Arc::new(AtomicBool::new(false)),
},
);
// Send desired SHM buffer parameters.
frame.buffer(
wl_shm::Format::Argb8888,
buffer_size.w as u32,
buffer_size.h as u32,
buffer_size.w as u32 * 4,
);
// if manager.version() >= 3 {
// // Send desired DMA buffer parameters.
// frame.linux_dmabuf(
// Fourcc::Argb8888 as u32,
// buffer_size.w as u32,
// buffer_size.h as u32,
// );
//
// // Notify client that all supported buffers were enumerated.
// frame.buffer_done();
// }
}
}
/// Handler trait for wlr-screencopy.
pub trait ScreencopyHandler {
/// Handle new screencopy request.
fn frame(&mut self, frame: Screencopy);
}
#[allow(missing_docs)]
#[macro_export]
macro_rules! delegate_screencopy {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: $crate::protocols::screencopy::ScreencopyManagerGlobalData
] => $crate::protocols::screencopy::ScreencopyManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: ()
] => $crate::protocols::screencopy::ScreencopyManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1: $crate::protocols::screencopy::ScreencopyFrameState
] => $crate::protocols::screencopy::ScreencopyManagerState);
};
}
#[derive(Clone)]
pub struct ScreencopyFrameInfo {
output: Output,
buffer_size: Size<i32, Physical>,
region_loc: Point<i32, Physical>,
overlay_cursor: bool,
}
pub enum ScreencopyFrameState {
Failed,
Pending {
info: ScreencopyFrameInfo,
copied: Arc<AtomicBool>,
},
}
impl<D> Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState, D> for ScreencopyManagerState
where
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
D: ScreencopyHandler,
D: 'static,
{
fn request(
state: &mut D,
_client: &Client,
frame: &ZwlrScreencopyFrameV1,
request: zwlr_screencopy_frame_v1::Request,
data: &ScreencopyFrameState,
_display: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
if matches!(request, zwlr_screencopy_frame_v1::Request::Destroy) {
return;
}
let (info, copied) = match data {
ScreencopyFrameState::Failed => return,
ScreencopyFrameState::Pending { info, copied } => (info, copied),
};
if copied.load(Ordering::SeqCst) {
frame.post_error(
zwlr_screencopy_frame_v1::Error::AlreadyUsed,
"copy was already requested",
);
return;
}
let (buffer, with_damage) = match request {
zwlr_screencopy_frame_v1::Request::Copy { buffer } => (buffer, false),
// zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
_ => unreachable!(),
};
if !shm::with_buffer_contents(&buffer, |_buf, shm_len, buffer_data| {
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == info.buffer_size.w * 4
&& buffer_data.height == info.buffer_size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height
})
.unwrap_or(false)
{
frame.post_error(
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
"invalid buffer",
);
return;
}
copied.store(true, Ordering::SeqCst);
state.frame(Screencopy {
with_damage,
buffer,
frame: frame.clone(),
info: info.clone(),
submitted: false,
});
}
}
/// Screencopy frame.
pub struct Screencopy {
info: ScreencopyFrameInfo,
frame: ZwlrScreencopyFrameV1,
#[allow(unused)]
with_damage: bool,
buffer: WlBuffer,
submitted: bool,
}
impl Drop for Screencopy {
fn drop(&mut self) {
if !self.submitted {
self.frame.failed();
}
}
}
impl Screencopy {
/// Get the target buffer to copy to.
pub fn buffer(&self) -> &WlBuffer {
&self.buffer
}
pub fn region_loc(&self) -> Point<i32, Physical> {
self.info.region_loc
}
pub fn buffer_size(&self) -> Size<i32, Physical> {
self.info.buffer_size
}
pub fn output(&self) -> &Output {
&self.info.output
}
pub fn overlay_cursor(&self) -> bool {
self.info.overlay_cursor
}
// pub fn damage(&mut self, damage: &[Rectangle<i32, Physical>]) {
// assert!(self.with_damage);
//
// for Rectangle { loc, size } in damage {
// self.frame
// .damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32);
// }
// }
/// Submit the copied content.
pub fn submit(mut self, y_invert: bool) {
// Notify client that buffer is ordinary.
self.frame.flags(if y_invert {
Flags::YInvert
} else {
Flags::empty()
});
// Notify client about successful copy.
let time = UNIX_EPOCH.elapsed().unwrap();
let tv_sec_hi = (time.as_secs() >> 32) as u32;
let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32;
let tv_nsec = time.subsec_nanos();
self.frame.ready(tv_sec_hi, tv_sec_lo, tv_nsec);
// Mark frame as submitted to ensure destructor isn't run.
self.submitted = true;
}
// pub fn submit_after_sync<T>(
// self,
// y_invert: bool,
// sync_point: Option<OwnedFd>,
// event_loop: &LoopHandle<'_, T>,
// ) {
// match sync_point {
// None => self.submit(y_invert),
// Some(sync_fd) => {
// let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
// let mut screencopy = Some(self);
// event_loop
// .insert_source(source, move |_, _, _| {
// screencopy.take().unwrap().submit(y_invert);
// Ok(PostAction::Remove)
// })
// .unwrap();
// }
// }
// }
}
+36 -20
View File
@@ -7,42 +7,48 @@ use std::rc::Rc;
use std::time::Duration;
use anyhow::Context as _;
use pipewire::spa::data::DataType;
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::context::Context;
use pipewire::core::Core;
use pipewire::main_loop::MainLoop;
use pipewire::properties::Properties;
use pipewire::spa::buffer::DataType;
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::spa::param::format_utils::parse_format;
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
use pipewire::spa::param::ParamType;
use pipewire::spa::pod::serialize::PodSerializer;
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
use pipewire::spa::sys::*;
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
use pipewire::spa::Direction;
use pipewire::spa::utils::{
Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Rectangle, SpaTypes,
};
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
use pipewire::{Context, Core, MainLoop, Properties};
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::DrmDeviceFd;
use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Size};
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
use crate::niri::State;
pub struct PipeWire {
_context: Context<MainLoop>,
_context: Context,
pub core: Core,
}
pub struct Cast {
pub session_id: usize,
pub stream: Rc<Stream>,
pub stream: Stream,
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub output: Output,
pub size: Size<i32, Physical>,
pub cursor_mode: CursorMode,
pub last_frame_time: Duration,
pub min_time_between_frames: Rc<Cell<Duration>>,
@@ -51,7 +57,7 @@ pub struct Cast {
impl PipeWire {
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
let main_loop = MainLoop::new().context("error creating MainLoop")?;
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
let context = Context::new(&main_loop).context("error creating Context")?;
let core = context.connect(None).context("error creating Core")?;
@@ -66,14 +72,14 @@ impl PipeWire {
struct AsFdWrapper(MainLoop);
impl AsFd for AsFdWrapper {
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.fd()
self.0.loop_().fd()
}
}
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
event_loop
.insert_source(generic, move |_, wrapper, _| {
let _span = tracy_client::span!("pipewire iteration");
wrapper.0.iterate(Duration::ZERO);
wrapper.0.loop_().iterate(Duration::ZERO);
Ok(PostAction::Continue)
})
.unwrap();
@@ -95,21 +101,31 @@ impl PipeWire {
) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast");
let to_niri_ = to_niri.clone();
let stop_cast = move || {
if let Err(err) = to_niri.send(ScreenCastToNiri::StopCast { session_id }) {
if let Err(err) = to_niri_.send(ScreenCastToNiri::StopCast { session_id }) {
warn!("error sending StopCast to niri: {err:?}");
}
};
let weak = output.downgrade();
let redraw = move || {
if let Some(output) = weak.upgrade() {
if let Err(err) = to_niri.send(ScreenCastToNiri::Redraw(output)) {
warn!("error sending Redraw to niri: {err:?}");
}
}
};
let mode = output.current_mode().unwrap();
let size = mode.size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let refresh = mode.refresh;
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
.context("error creating Stream")?;
// Like in good old wayland-rs times...
let stream = Rc::new(stream);
let node_id = Rc::new(Cell::new(None));
let is_active = Rc::new(Cell::new(false));
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
@@ -118,10 +134,9 @@ impl PipeWire {
let listener = stream
.add_local_listener_with_user_data(())
.state_changed({
let stream = stream.clone();
let is_active = is_active.clone();
let stop_cast = stop_cast.clone();
move |old, new| {
move |stream, (), old, new| {
debug!("pw stream: state changed: {old:?} -> {new:?}");
match new {
@@ -158,13 +173,14 @@ impl PipeWire {
StreamState::Connecting => (),
StreamState::Streaming => {
is_active.set(true);
redraw();
}
}
}
})
.param_changed({
let min_time_between_frames = min_time_between_frames.clone();
move |stream, id, _data, pod| {
move |stream, (), id, pod| {
let id = ParamType::from_raw(id);
trace!(?id, "pw stream: param_changed");
@@ -246,8 +262,7 @@ impl PipeWire {
let mut b1 = vec![];
// let mut b2 = vec![];
let mut params = [
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
make_pod(&mut b1, o1), // make_pod(&mut b2, o2)
];
stream.update_params(&mut params).unwrap();
}
@@ -255,7 +270,7 @@ impl PipeWire {
.add_buffer({
let dmabufs = dmabufs.clone();
let stop_cast = stop_cast.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: add_buffer");
unsafe {
@@ -299,7 +314,7 @@ impl PipeWire {
})
.remove_buffer({
let dmabufs = dmabufs.clone();
move |buffer| {
move |_stream, (), buffer| {
trace!("pw stream: remove_buffer");
unsafe {
@@ -373,6 +388,7 @@ impl PipeWire {
_listener: listener,
is_active,
output,
size,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
+135
View File
@@ -0,0 +1,135 @@
use glam::Vec2;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_pixel_shader::PrimaryGpuPixelShaderRenderElement;
use super::renderer::NiriRenderer;
use super::shaders::Shaders;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders a sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
#[derive(Debug)]
pub struct GradientRenderElement(PrimaryGpuPixelShaderRenderElement);
impl GradientRenderElement {
pub fn new(
renderer: &mut impl NiriRenderer,
scale: Scale<f64>,
area: Rectangle<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
angle: f32,
) -> Option<Self> {
let shader = Shaders::get(renderer).gradient_border.clone()?;
let grad_offset = (area.loc - gradient_area.loc).to_f64().to_physical(scale);
let grad_dir = Vec2::from_angle(angle);
let grad_area_size = gradient_area.size.to_f64().to_physical(scale);
let (w, h) = (grad_area_size.w as f32, grad_area_size.h as f32);
let mut grad_area_diag = Vec2::new(w, h);
if (grad_dir.x < 0. && 0. <= grad_dir.y) || (0. <= grad_dir.x && grad_dir.y < 0.) {
grad_area_diag.x = -w;
}
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
if grad_dir.y <= 0. {
grad_vec = -grad_vec;
}
let elem = PixelShaderElement::new(
shader,
area,
None,
1.,
vec![
Uniform::new("color_from", color_from),
Uniform::new("color_to", color_to),
Uniform::new("grad_offset", (grad_offset.x as f32, grad_offset.y as f32)),
Uniform::new("grad_width", w),
Uniform::new("grad_vec", grad_vec.to_array()),
],
Kind::Unspecified,
);
Some(Self(PrimaryGpuPixelShaderRenderElement(elem)))
}
}
impl Element for GradientRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for GradientRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for GradientRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
RenderElement::<TtyRenderer<'_>>::draw(&self.0, frame, src, dst, damage)
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
self.0.underlying_storage(renderer)
}
}
+180
View File
@@ -0,0 +1,180 @@
use std::ptr;
use anyhow::{ensure, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{buffer_dimensions, Bind, ExportMem, Frame, Offscreen, Renderer};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::utils::{Physical, Rectangle, Scale, Size, Transform};
use smithay::wayland::shm;
pub mod gradient;
pub mod offscreen;
pub mod primary_gpu_pixel_shader;
pub mod primary_gpu_texture;
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>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
let _span = tracy_client::span!();
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let texture: GlesTexture = renderer
.create_buffer(fourcc, buffer_size)
.context("error creating texture")?;
renderer
.bind(texture.clone())
.context("error binding texture")?;
let sync_point = render_elements(renderer, size, scale, transform, elements)?;
Ok((texture, sync_point))
}
pub fn render_and_download(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<GlesMapping> {
let _span = tracy_client::span!();
let (_, sync_point) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
sync_point.wait();
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let mapping = renderer
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
}
pub fn render_to_vec(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
fourcc: Fourcc,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<Vec<u8>> {
let _span = tracy_client::span!();
let mapping = render_and_download(renderer, size, scale, transform, fourcc, elements)
.context("error rendering")?;
let copy = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
Ok(copy.to_vec())
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn render_to_dmabuf(
renderer: &mut GlesRenderer,
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let _span = tracy_client::span!();
renderer.bind(dmabuf).context("error binding texture")?;
render_elements(renderer, size, scale, transform, elements)
}
pub fn render_to_shm(
renderer: &mut GlesRenderer,
buffer: &WlBuffer,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<()> {
let _span = tracy_client::span!();
let buffer_size = buffer_dimensions(buffer).context("error getting buffer dimensions")?;
let size = buffer_size.to_logical(1, Transform::Normal).to_physical(1);
let mapping =
render_and_download(renderer, size, scale, transform, Fourcc::Argb8888, elements)?;
let bytes = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
ensure!(
// The buffer prefers pixels in little endian ...
buffer_data.format == wl_shm::Format::Argb8888
&& buffer_data.stride == size.w * 4
&& buffer_data.height == size.h
&& shm_len as i32 == buffer_data.stride * buffer_data.height,
"invalid buffer format or size"
);
ensure!(bytes.len() == shm_len, "mapped buffer has wrong length");
unsafe {
let _span = tracy_client::span!("copy_nonoverlapping");
ptr::copy_nonoverlapping(bytes.as_ptr(), shm_buffer.cast(), shm_len);
}
Ok(())
})
.context("expected shm buffer, but didn't get one")?
}
fn render_elements(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
) -> anyhow::Result<SyncPoint> {
let transform = transform.invert();
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
let mut frame = renderer
.render(size, transform)
.context("error starting frame")?;
frame
.clear([0., 0., 0., 0.], &[output_rect])
.context("error clearing")?;
for element in elements {
let src = element.src();
let dst = element.geometry(scale);
if let Some(mut damage) = output_rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
frame.finish().context("error finishing frame")
}
+216
View File
@@ -0,0 +1,216 @@
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use super::render_to_texture;
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Renders elements into an off-screen buffer.
#[derive(Debug)]
pub struct OffscreenRenderElement {
// The texture, if rendering succeeded.
texture: Option<PrimaryGpuTextureRenderElement>,
// The fallback buffer in case the rendering fails.
fallback: SolidColorRenderElement,
}
impl OffscreenRenderElement {
pub fn new(
renderer: &mut GlesRenderer,
scale: i32,
elements: &[impl RenderElement<GlesRenderer>],
result_alpha: f32,
) -> Self {
let _span = tracy_client::span!("OffscreenRenderElement::new");
let geo = elements
.iter()
.map(|ele| ele.geometry(Scale::from(f64::from(scale))))
.reduce(|a, b| a.merge(b))
.unwrap_or_default();
let logical_size = geo.size.to_logical(scale);
let fallback_buffer = SolidColorBuffer::new(logical_size, [1., 0., 0., 1.]);
let fallback = SolidColorRenderElement::from_buffer(
&fallback_buffer,
geo.loc,
Scale::from(scale as f64),
result_alpha,
Kind::Unspecified,
);
let elements = elements.iter().rev().map(|ele| {
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
});
match render_to_texture(
renderer,
geo.size,
Scale::from(scale as f64),
Transform::Normal,
Fourcc::Abgr8888,
elements,
) {
Ok((texture, _sync_point)) => {
let buffer =
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
let element = TextureRenderElement::from_texture_buffer(
geo.loc.to_f64(),
&buffer,
Some(result_alpha),
None,
None,
Kind::Unspecified,
);
Self {
texture: Some(PrimaryGpuTextureRenderElement(element)),
fallback,
}
}
Err(err) => {
warn!("error off-screening elements: {err:?}");
Self {
texture: None,
fallback,
}
}
}
}
}
impl Element for OffscreenRenderElement {
fn id(&self) -> &Id {
if let Some(texture) = &self.texture {
texture.id()
} else {
self.fallback.id()
}
}
fn current_commit(&self) -> CommitCounter {
if let Some(texture) = &self.texture {
texture.current_commit()
} else {
self.fallback.current_commit()
}
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
if let Some(texture) = &self.texture {
texture.geometry(scale)
} else {
self.fallback.geometry(scale)
}
}
fn transform(&self) -> Transform {
if let Some(texture) = &self.texture {
texture.transform()
} else {
self.fallback.transform()
}
}
fn src(&self) -> Rectangle<f64, Buffer> {
if let Some(texture) = &self.texture {
texture.src()
} else {
self.fallback.src()
}
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
if let Some(texture) = &self.texture {
texture.damage_since(scale, commit)
} else {
self.fallback.damage_since(scale, commit)
}
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
if let Some(texture) = &self.texture {
texture.opaque_regions(scale)
} else {
self.fallback.opaque_regions(scale)
}
}
fn alpha(&self) -> f32 {
if let Some(texture) = &self.texture {
texture.alpha()
} else {
self.fallback.alpha()
}
}
fn kind(&self) -> Kind {
if let Some(texture) = &self.texture {
texture.kind()
} else {
self.fallback.kind()
}
}
}
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
}
Ok(())
}
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
if let Some(texture) = &self.texture {
RenderElement::<GlesRenderer>::draw(texture, gles_frame, src, dst, damage)?;
} else {
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
}
Ok(())
}
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
if let Some(texture) = &self.texture {
texture.underlying_storage(renderer)
} else {
self.fallback.underlying_storage(renderer)
}
}
}
@@ -0,0 +1,97 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::element::PixelShaderElement;
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a poxel shader from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuPixelShaderRenderElement(pub PixelShaderElement);
impl Element for PrimaryGpuPixelShaderRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuPixelShaderRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
+97
View File
@@ -0,0 +1,97 @@
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
#[derive(Debug)]
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
impl Element for PrimaryGpuTextureRenderElement {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn transform(&self) -> Transform {
self.0.transform()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> Vec<Rectangle<i32, Physical>> {
self.0.damage_since(scale, commit)
}
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
self.0.opaque_regions(scale)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
Ok(())
}
fn underlying_storage(
&self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage> {
// If scanout for things other than Wayland buffers is implemented, this will need to take
// the target GPU into account.
None
}
}
+148
View File
@@ -0,0 +1,148 @@
// We need to implement RenderElement manually due to AsGlesFrame requirement.
// This macro does it for us.
#[macro_export]
macro_rules! niri_render_elements {
// The two callable variants: with <R> and without <R>. They include From impls because nested
// repetitions ($type and $variant with + and $R with ?) don't work properly.
($name:ident<R> => { $($variant:ident = $type:ty),+ $(,)? }) => {
$crate::niri_render_elements!(@impl $name () ($name<R>) => { $($variant = $type),+ });
$(impl<R: $crate::render_helpers::renderer::NiriRenderer> From<$type> for $name<R> {
fn from(x: $type) -> Self {
Self::$variant(x)
}
})+
};
($name:ident => { $($variant:ident = $type:ty),+ $(,)? }) => {
$crate::niri_render_elements!(@impl $name ($name) () => { $($variant = $type),+ });
$(impl From<$type> for $name {
fn from(x: $type) -> Self {
Self::$variant(x)
}
})+
};
// The internal variant that generates most of the code. $name_no_R and $name_R are necessary
// for the impl RenderElement<SomeRenderer> for $name<SomeRenderer>: since $R does not appear
// in this line, we cannot condition based on $R like elsewhere, so we condition on duplicate
// names instead. Like this: $($name_R<SomeRenderer>)? $($name_no_R)? so only one is chosen.
(@impl $name:ident ($($name_no_R:ident)?) ($($name_R:ident<$R:ident>)?) => { $($variant:ident = $type:ty),+ }) => {
#[derive(Debug)]
pub enum $name$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? {
$($variant($type)),+
}
impl$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? smithay::backend::renderer::element::Element for $name$(<$R>)? {
fn id(&self) -> &smithay::backend::renderer::element::Id {
match self {
$($name::$variant(elem) => elem.id()),+
}
}
fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter {
match self {
$($name::$variant(elem) => elem.current_commit()),+
}
}
fn geometry(&self, scale: smithay::utils::Scale<f64>) -> smithay::utils::Rectangle<i32, smithay::utils::Physical> {
match self {
$($name::$variant(elem) => elem.geometry(scale)),+
}
}
fn transform(&self) -> smithay::utils::Transform {
match self {
$($name::$variant(elem) => elem.transform()),+
}
}
fn src(&self) -> smithay::utils::Rectangle<f64, smithay::utils::Buffer> {
match self {
$($name::$variant(elem) => elem.src()),+
}
}
fn damage_since(
&self,
scale: smithay::utils::Scale<f64>,
commit: Option<smithay::backend::renderer::utils::CommitCounter>,
) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
match self {
$($name::$variant(elem) => elem.damage_since(scale, commit)),+
}
}
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
match self {
$($name::$variant(elem) => elem.opaque_regions(scale)),+
}
}
fn alpha(&self) -> f32 {
match self {
$($name::$variant(elem) => elem.alpha()),+
}
}
fn kind(&self) -> smithay::backend::renderer::element::Kind {
match self {
$($name::$variant(elem) => elem.kind()),+
}
}
}
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer>
for $($name_R<smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_R)?
{
fn draw(
&self,
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage)
})+
}
}
fn underlying_storage(&self, renderer: &mut smithay::backend::renderer::gles::GlesRenderer) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
match self {
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
}
}
}
impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>>
for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)?
{
fn draw(
&self,
frame: &mut $crate::backend::tty::TtyFrame<'render, '_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage)
})+
}
}
fn underlying_storage(
&self,
renderer: &mut $crate::backend::tty::TtyRenderer<'render>,
) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
match self {
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
}
}
}
};
}
+73
View File
@@ -0,0 +1,73 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::{
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
};
use crate::backend::tty::{TtyFrame, TtyRenderer};
/// Trait with our main renderer requirements to save on the typing.
pub trait NiriRenderer:
ImportAll
+ ImportMem
+ ExportMem
+ Bind<Dmabuf>
+ Offscreen<GlesTexture>
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
+ AsGlesRenderer
{
// Associated types to work around the instability of associated type bounds.
type NiriTextureId: Texture + Clone + 'static;
type NiriError: std::error::Error
+ Send
+ Sync
+ From<<GlesRenderer as Renderer>::Error>
+ 'static;
}
impl<R> NiriRenderer for R
where
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
R::TextureId: Texture + Clone + 'static,
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
{
type NiriTextureId = R::TextureId;
type NiriError = R::Error;
}
/// Trait for getting the underlying `GlesRenderer`.
pub trait AsGlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
}
impl AsGlesRenderer for GlesRenderer {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self
}
}
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
self.as_mut()
}
}
/// Trait for getting the underlying `GlesFrame`.
pub trait AsGlesFrame<'frame>
where
Self: 'frame,
{
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
}
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self
}
}
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
self.as_mut()
}
}
@@ -0,0 +1,35 @@
precision mediump float;
uniform float alpha;
#if defined(DEBUG_FLAGS)
uniform float tint;
#endif
uniform vec2 size;
varying vec2 v_coords;
uniform vec4 color_from;
uniform vec4 color_to;
uniform vec2 grad_offset;
uniform float grad_width;
uniform vec2 grad_vec;
void main() {
vec2 coords = v_coords * size + grad_offset;
if ((grad_vec.x < 0.0 && 0.0 <= grad_vec.y) || (0.0 <= grad_vec.x && grad_vec.y < 0.0))
coords.x -= grad_width;
float frac = dot(coords, grad_vec) / dot(grad_vec, grad_vec);
if (grad_vec.y < 0.0)
frac += 1.0;
frac = clamp(frac, 0.0, 1.0);
vec4 out_color = mix(color_from, color_to, frac);
#if defined(DEBUG_FLAGS)
if (tint == 1.0)
out_color = vec4(0.0, 0.3, 0.0, 0.2) + out_color * 0.8;
#endif
gl_FragColor = out_color;
}
+46
View File
@@ -0,0 +1,46 @@
use smithay::backend::renderer::gles::{GlesPixelProgram, GlesRenderer, UniformName, UniformType};
use super::renderer::NiriRenderer;
pub struct Shaders {
pub gradient_border: Option<GlesPixelProgram>,
}
impl Shaders {
fn compile(renderer: &mut GlesRenderer) -> Self {
let _span = tracy_client::span!("Shaders::compile");
let gradient_border = renderer
.compile_custom_pixel_shader(
include_str!("gradient_border.frag"),
&[
UniformName::new("color_from", UniformType::_4f),
UniformName::new("color_to", UniformType::_4f),
UniformName::new("grad_offset", UniformType::_2f),
UniformName::new("grad_width", UniformType::_1f),
UniformName::new("grad_vec", UniformType::_2f),
],
)
.map_err(|err| {
warn!("error compiling gradient border shader: {err:?}");
})
.ok();
Self { gradient_border }
}
pub fn get(renderer: &mut impl NiriRenderer) -> &Self {
let renderer = renderer.as_gles_renderer();
let data = renderer.egl_context().user_data();
data.get()
.expect("shaders::init() must be called when creating the renderer")
}
}
pub fn init(renderer: &mut GlesRenderer) {
let shaders = Shaders::compile(renderer);
let data = renderer.egl_context().user_data();
if !data.insert_if_missing(|| shaders) {
error!("shaders were already compiled");
}
}
+39
View File
@@ -0,0 +1,39 @@
#[derive(Debug, Clone, Copy)]
pub struct RubberBand {
pub stiffness: f64,
pub limit: f64,
}
impl RubberBand {
pub fn band(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
(1. - (1. / (x * c / d + 1.))) * d
}
pub fn derivative(&self, x: f64) -> f64 {
let c = self.stiffness;
let d = self.limit;
c * d * d / (c * x + d).powi(2)
}
pub fn clamp(&self, min: f64, max: f64, x: f64) -> f64 {
let clamped = x.clamp(min, max);
let sign = if x < clamped { -1. } else { 1. };
let diff = (x - clamped).abs();
clamped + sign * self.band(diff)
}
pub fn clamp_derivative(&self, min: f64, max: f64, x: f64) -> f64 {
if min <= x && x <= max {
return 1.;
}
let clamped = x.clamp(min, max);
let diff = (x - clamped).abs();
self.derivative(diff)
}
}
+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.;
}
}
+87
View File
@@ -0,0 +1,87 @@
use std::collections::VecDeque;
use std::time::Duration;
const HISTORY_LIMIT: Duration = Duration::from_millis(150);
const DECELERATION_TOUCHPAD: f64 = 0.997;
#[derive(Debug)]
pub struct SwipeTracker {
history: VecDeque<Event>,
pos: f64,
}
#[derive(Debug, Clone, Copy)]
struct Event {
delta: f64,
timestamp: Duration,
}
impl SwipeTracker {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
history: VecDeque::new(),
pos: 0.,
}
}
/// Pushes a new reading into the tracker.
pub fn push(&mut self, delta: f64, timestamp: Duration) {
// For the events that we care about, timestamps should always increase
// monotonically.
if let Some(last) = self.history.back() {
if timestamp < last.timestamp {
trace!(
"ignoring event with timestamp {timestamp:?} earlier than last {:?}",
last.timestamp
);
return;
}
}
self.history.push_back(Event { delta, timestamp });
self.pos += delta;
self.trim_history();
}
/// Returns the current gesture position.
pub fn pos(&self) -> f64 {
self.pos
}
/// Computes the current gesture velocity.
pub fn velocity(&self) -> f64 {
let (Some(first), Some(last)) = (self.history.front(), self.history.back()) else {
return 0.;
};
let total_time = (last.timestamp - first.timestamp).as_secs_f64();
if total_time == 0. {
return 0.;
}
let total_delta = self.history.iter().map(|event| event.delta).sum::<f64>();
total_delta / total_time
}
/// Computes the gesture end position after decelerating to a halt.
pub fn projected_end_pos(&self) -> f64 {
let vel = self.velocity();
self.pos - vel / (1000. * DECELERATION_TOUCHPAD.ln())
}
fn trim_history(&mut self) {
let Some(&Event { timestamp, .. }) = self.history.back() else {
return;
};
while let Some(first) = self.history.front() {
if timestamp <= first.timestamp + HISTORY_LIMIT {
break;
}
let _ = self.history.pop_front();
}
}
}
+255
View File
@@ -0,0 +1,255 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::Duration;
use niri_config::Config;
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::FontDescription;
use smithay::backend::renderer::element::memory::{
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Kind};
use smithay::output::Output;
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::animation::Animation;
use crate::render_helpers::renderer::NiriRenderer;
const TEXT: &str = "Failed to parse the config file. \
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
to see the errors.";
const PADDING: i32 = 8;
const FONT: &str = "sans 14px";
const BORDER: i32 = 4;
pub struct ConfigErrorNotification {
state: State,
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
// notification.
created_path: Option<PathBuf>,
config: Rc<RefCell<Config>>,
}
enum State {
Hidden,
Showing(Animation),
Shown(Duration),
Hiding(Animation),
}
pub type ConfigErrorNotificationRenderElement<R> =
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl ConfigErrorNotification {
pub fn new(config: Rc<RefCell<Config>>) -> Self {
Self {
state: State::Hidden,
buffers: RefCell::new(HashMap::new()),
created_path: None,
config,
}
}
fn animation(&self, from: f64, to: f64) -> Animation {
let c = self.config.borrow();
Animation::new(
from,
to,
0.,
c.animations.config_notification_open_close,
niri_config::Animation::default_config_notification_open_close(),
)
}
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
if self.created_path != created_path {
self.created_path = created_path;
self.buffers.borrow_mut().clear();
}
self.state = State::Showing(self.animation(0., 1.));
}
pub fn show(&mut self) {
if self.created_path.is_some() {
self.created_path = None;
self.buffers.borrow_mut().clear();
}
// Show from scratch even if already showing to bring attention.
self.state = State::Showing(self.animation(0., 1.));
}
pub fn hide(&mut self) {
if matches!(self.state, State::Hidden) {
return;
}
self.state = State::Hiding(self.animation(1., 0.));
}
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
match &mut self.state {
State::Hidden => (),
State::Showing(anim) => {
anim.set_current_time(target_presentation_time);
if anim.is_done() {
let duration = if self.created_path.is_some() {
// Make this quite a bit longer because it comes with a monitor modeset
// (can take a while) and an important hotkeys popup diverting the
// attention.
Duration::from_secs(8)
} else {
Duration::from_secs(4)
};
self.state = State::Shown(target_presentation_time + duration);
}
}
State::Shown(deadline) => {
if target_presentation_time >= *deadline {
self.hide();
}
}
State::Hiding(anim) => {
anim.set_current_time(target_presentation_time);
// HACK: prevent bounciness on hiding. This is better done with a clamp property on
// the spring animation.
if anim.is_done() || anim.value() <= 0. {
self.state = State::Hidden;
}
}
}
}
pub fn are_animations_ongoing(&self) -> bool {
!matches!(self.state, State::Hidden)
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
) -> Option<ConfigErrorNotificationRenderElement<R>> {
if matches!(self.state, State::Hidden) {
return None;
}
let scale = output.current_scale().integer_scale();
let path = self.created_path.as_deref();
let mut buffers = self.buffers.borrow_mut();
let buffer = buffers
.entry(scale)
.or_insert_with_key(move |&scale| render(scale, path).ok());
let buffer = buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer,
(0., 0.),
buffer,
Some(0.9),
None,
None,
Kind::Unspecified,
)
.ok()?;
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let output_size = output_transform.transform_size(output_mode.size);
let buffer_size = elem
.geometry(output.current_scale().fractional_scale().into())
.size;
let y_range = buffer_size.h + PADDING * 2 * scale;
let x = (output_size.w / 2 - buffer_size.w / 2).max(0);
let y = match &self.state {
State::Hidden => unreachable!(),
State::Showing(anim) | State::Hiding(anim) => {
(-buffer_size.h as f64 + anim.value() * y_range as f64).round() as i32
}
State::Shown(_) => PADDING * 2 * scale,
};
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
Some(elem)
}
}
fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("config_error_notification::render");
let padding = PADDING * scale;
let mut text = String::from(TEXT);
let mut border_color = (1., 0.3, 0.3);
if let Some(path) = created_path {
text = format!(
"Created a default config file at \
<span face='monospace' bgcolor='#000000'>{:?}</span>",
path
);
border_color = (0.5, 1., 0.5);
};
let mut font = FontDescription::from_string(FONT);
font.set_absolute_size((font.size() * scale).into());
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(&text);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
height += padding * 2;
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
width = (width + scale - 1) / scale * scale;
height = (height + scale - 1) / scale * scale;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_markup(&text);
cr.set_source_rgb(1., 1., 1.);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
cr.line_to(width.into(), height.into());
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(border_color.0, border_color.1, border_color.2);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(buffer)
}
+162
View File
@@ -0,0 +1,162 @@
use std::cell::RefCell;
use std::collections::HashMap;
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::{Alignment, FontDescription};
use smithay::backend::renderer::element::memory::{
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Kind};
use smithay::output::Output;
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::Transform;
use crate::render_helpers::renderer::NiriRenderer;
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
const PADDING: i32 = 16;
const FONT: &str = "sans 14px";
const BORDER: i32 = 8;
pub struct ExitConfirmDialog {
is_open: bool,
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
}
pub type ExitConfirmDialogRenderElement<R> =
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl ExitConfirmDialog {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
is_open: false,
buffers: RefCell::new(HashMap::from([(1, Some(render(1)?))])),
})
}
pub fn show(&mut self) -> bool {
if !self.is_open {
self.is_open = true;
true
} else {
false
}
}
pub fn hide(&mut self) -> bool {
if self.is_open {
self.is_open = false;
true
} else {
false
}
}
pub fn is_open(&self) -> bool {
self.is_open
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
) -> Option<ExitConfirmDialogRenderElement<R>> {
if !self.is_open {
return None;
}
let scale = output.current_scale().integer_scale();
let mut buffers = self.buffers.borrow_mut();
let fallback = buffers[&1].clone().unwrap();
let buffer = buffers.entry(scale).or_insert_with(|| render(scale).ok());
let buffer = buffer.as_ref().unwrap_or(&fallback);
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer,
(0., 0.),
buffer,
None,
None,
None,
Kind::Unspecified,
)
.ok()?;
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let output_size = output_transform.transform_size(output_mode.size);
let buffer_size = elem
.geometry(output.current_scale().fractional_scale().into())
.size;
let x = (output_size.w / 2 - buffer_size.w / 2).max(0);
let y = (output_size.h / 2 - buffer_size.h / 2).max(0);
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
Some(elem)
}
}
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
let _span = tracy_client::span!("exit_confirm_dialog::render");
let padding = PADDING * scale;
let mut font = FontDescription::from_string(FONT);
font.set_absolute_size((font.size() * scale).into());
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_markup(TEXT);
let (mut width, mut height) = layout.pixel_size();
width += padding * 2;
height += padding * 2;
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
width = (width + scale - 1) / scale * scale;
height = (height + scale - 1) / scale * scale;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
layout.set_alignment(Alignment::Center);
layout.set_markup(TEXT);
cr.set_source_rgb(1., 1., 1.);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
cr.line_to(width.into(), height.into());
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(1., 0.3, 0.3);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(buffer)
}
+456
View File
@@ -0,0 +1,456 @@
use std::cell::RefCell;
use std::cmp::max;
use std::collections::HashMap;
use std::iter::zip;
use std::rc::Rc;
use niri_config::{Action, Config, Key, Modifiers, Trigger};
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
use smithay::backend::renderer::element::memory::{
MemoryRenderBuffer, MemoryRenderBufferRenderElement,
};
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::input::keyboard::xkb::keysym_get_name;
use smithay::output::{Output, WeakOutput};
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::{Physical, Size, Transform};
use crate::input::CompositorMod;
use crate::render_helpers::renderer::NiriRenderer;
const PADDING: i32 = 8;
const MARGIN: i32 = PADDING * 2;
const FONT: &str = "sans 14px";
const BORDER: i32 = 4;
const LINE_INTERVAL: i32 = 2;
const TITLE: &str = "Important Hotkeys";
pub struct HotkeyOverlay {
is_open: bool,
config: Rc<RefCell<Config>>,
comp_mod: CompositorMod,
buffers: RefCell<HashMap<WeakOutput, RenderedOverlay>>,
}
pub struct RenderedOverlay {
buffer: Option<MemoryRenderBuffer>,
size: Size<i32, Physical>,
scale: i32,
}
pub type HotkeyOverlayRenderElement<R> = RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
impl HotkeyOverlay {
pub fn new(config: Rc<RefCell<Config>>, comp_mod: CompositorMod) -> Self {
Self {
is_open: false,
config,
comp_mod,
buffers: RefCell::new(HashMap::new()),
}
}
pub fn show(&mut self) -> bool {
if !self.is_open {
self.is_open = true;
true
} else {
false
}
}
pub fn hide(&mut self) -> bool {
if self.is_open {
self.is_open = false;
true
} else {
false
}
}
pub fn is_open(&self) -> bool {
self.is_open
}
pub fn on_hotkey_config_updated(&mut self) {
self.buffers.borrow_mut().clear();
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
output: &Output,
) -> Option<HotkeyOverlayRenderElement<R>> {
if !self.is_open {
return None;
}
let scale = output.current_scale().integer_scale();
let margin = MARGIN * scale;
let output_transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let output_size = output_transform.transform_size(output_mode.size);
let mut buffers = self.buffers.borrow_mut();
buffers.retain(|output, _| output.upgrade().is_some());
// FIXME: should probably use the working area rather than view size.
let weak = output.downgrade();
if let Some(rendered) = buffers.get(&weak) {
if rendered.scale != scale {
buffers.remove(&weak);
}
}
let rendered = buffers.entry(weak).or_insert_with(|| {
render(&self.config.borrow(), self.comp_mod, scale).unwrap_or_else(|_| {
// This can go negative but whatever, as long as there's no rerender loop.
let mut size = output_size;
size.w -= margin * 2;
size.h -= margin * 2;
RenderedOverlay {
buffer: None,
size,
scale,
}
})
});
let buffer = rendered.buffer.as_ref()?;
let elem = MemoryRenderBufferRenderElement::from_buffer(
renderer,
(0., 0.),
buffer,
Some(0.9),
None,
None,
Kind::Unspecified,
)
.ok()?;
let x = (output_size.w / 2 - rendered.size.w / 2).max(0);
let y = (output_size.h / 2 - rendered.size.h / 2).max(0);
let elem = RelocateRenderElement::from_element(elem, (x, y), Relocate::Absolute);
Some(elem)
}
}
fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Result<RenderedOverlay> {
let _span = tracy_client::span!("hotkey_overlay::render");
// let margin = MARGIN * scale;
let padding = PADDING * scale;
let line_interval = LINE_INTERVAL * scale;
// FIXME: if it doesn't fit, try splitting in two columns or something.
// let mut target_size = output_size;
// target_size.w -= margin * 2;
// target_size.h -= margin * 2;
// anyhow::ensure!(target_size.w > 0 && target_size.h > 0);
let binds = &config.binds.0;
// Collect actions that we want to show.
let mut actions = vec![&Action::ShowHotkeyOverlay];
// Prefer Quit(false) if found, otherwise try Quit(true), and if there's neither, fall back to
// Quit(false).
if binds.iter().any(|bind| bind.action == Action::Quit(false)) {
actions.push(&Action::Quit(false));
} else if binds.iter().any(|bind| bind.action == Action::Quit(true)) {
actions.push(&Action::Quit(true));
} else {
actions.push(&Action::Quit(false));
}
actions.extend(&[
&Action::CloseWindow,
&Action::FocusColumnLeft,
&Action::FocusColumnRight,
&Action::MoveColumnLeft,
&Action::MoveColumnRight,
&Action::FocusWorkspaceDown,
&Action::FocusWorkspaceUp,
]);
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
if binds
.iter()
.any(|bind| bind.action == Action::MoveColumnToWorkspaceDown)
{
actions.push(&Action::MoveColumnToWorkspaceDown);
} else if binds
.iter()
.any(|bind| bind.action == Action::MoveWindowToWorkspaceDown)
{
actions.push(&Action::MoveWindowToWorkspaceDown);
} else {
actions.push(&Action::MoveColumnToWorkspaceDown);
}
// Same for -up.
if binds
.iter()
.any(|bind| bind.action == Action::MoveColumnToWorkspaceUp)
{
actions.push(&Action::MoveColumnToWorkspaceUp);
} else if binds
.iter()
.any(|bind| bind.action == Action::MoveWindowToWorkspaceUp)
{
actions.push(&Action::MoveWindowToWorkspaceUp);
} else {
actions.push(&Action::MoveColumnToWorkspaceUp);
}
actions.extend(&[
&Action::SwitchPresetColumnWidth,
&Action::MaximizeColumn,
&Action::ConsumeWindowIntoColumn,
&Action::ExpelWindowFromColumn,
]);
// Screenshot is not as important, can omit if not bound.
if binds.iter().any(|bind| bind.action == Action::Screenshot) {
actions.push(&Action::Screenshot);
}
// Add the spawn actions.
let mut spawn_actions = Vec::new();
for bind in binds.iter().filter(|bind| {
matches!(bind.action, Action::Spawn(_))
// 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;
// We only show one bind for each action, so we need to deduplicate the Spawn actions.
if !spawn_actions.contains(&action) {
spawn_actions.push(action);
}
}
actions.extend(spawn_actions);
let strings = actions
.into_iter()
.map(|action| {
let key = config
.binds
.0
.iter()
.find(|bind| bind.action == *action)
.map(|bind| key_name(comp_mod, &bind.key))
.unwrap_or_else(|| String::from("(not bound)"));
(format!(" {key} "), action_name(action))
})
.collect::<Vec<_>>();
let mut font = FontDescription::from_string(FONT);
font.set_absolute_size((font.size() * scale).into());
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
let bold = AttrList::new();
bold.insert(AttrInt::new_weight(Weight::Bold));
layout.set_attributes(Some(&bold));
layout.set_text(TITLE);
let title_size = layout.pixel_size();
let attrs = AttrList::new();
attrs.insert(AttrString::new_family("Monospace"));
attrs.insert(AttrColor::new_background(12000, 12000, 12000));
layout.set_attributes(Some(&attrs));
let key_sizes = strings
.iter()
.map(|(key, _)| {
layout.set_text(key);
layout.pixel_size()
})
.collect::<Vec<_>>();
layout.set_attributes(None);
let action_sizes = strings
.iter()
.map(|(_, action)| {
layout.set_markup(action);
layout.pixel_size()
})
.collect::<Vec<_>>();
let key_width = key_sizes.iter().map(|(w, _)| w).max().unwrap();
let action_width = action_sizes.iter().map(|(w, _)| w).max().unwrap();
let mut width = key_width + padding + action_width;
let mut height = zip(&key_sizes, &action_sizes)
.map(|((_, key_h), (_, act_h))| max(key_h, act_h))
.sum::<i32>()
+ (key_sizes.len() - 1) as i32 * line_interval
+ title_size.1
+ padding;
width += padding * 2;
height += padding * 2;
// FIXME: fix bug in Smithay that rounds pixel sizes down to scale.
width = (width + scale - 1) / scale * scale;
height = (height + scale - 1) / scale * scale;
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(0.1, 0.1, 0.1);
cr.paint()?;
cr.move_to(padding.into(), padding.into());
let layout = pangocairo::functions::create_layout(&cr);
layout.set_font_description(Some(&font));
cr.set_source_rgb(1., 1., 1.);
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
layout.set_attributes(Some(&bold));
layout.set_text(TITLE);
pangocairo::functions::show_layout(&cr, &layout);
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
layout.set_attributes(Some(&attrs));
layout.set_text(key);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to((key_width + padding).into(), 0.);
layout.set_attributes(None);
layout.set_markup(action);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to(
(-(key_width + padding)).into(),
(max(key_h, act_h) + line_interval).into(),
);
}
cr.move_to(0., 0.);
cr.line_to(width.into(), 0.);
cr.line_to(width.into(), height.into());
cr.line_to(0., height.into());
cr.line_to(0., 0.);
cr.set_source_rgb(0.5, 0.8, 1.0);
cr.set_line_width((BORDER * scale).into());
cr.stroke()?;
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
scale,
Transform::Normal,
None,
);
Ok(RenderedOverlay {
buffer: Some(buffer),
size: Size::from((width, height)),
scale,
})
}
fn action_name(action: &Action) -> String {
match action {
Action::Quit(_) => String::from("Exit niri"),
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
Action::CloseWindow => String::from("Close Focused Window"),
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
Action::FocusColumnRight => String::from("Focus Column to the Right"),
Action::MoveColumnLeft => String::from("Move Column Left"),
Action::MoveColumnRight => String::from("Move Column Right"),
Action::FocusWorkspaceDown => String::from("Switch Workspace Down"),
Action::FocusWorkspaceUp => String::from("Switch Workspace Up"),
Action::MoveColumnToWorkspaceDown => String::from("Move Column to Workspace Down"),
Action::MoveColumnToWorkspaceUp => String::from("Move Column to Workspace Up"),
Action::MoveWindowToWorkspaceDown => String::from("Move Window to Workspace Down"),
Action::MoveWindowToWorkspaceUp => String::from("Move Window to Workspace Up"),
Action::SwitchPresetColumnWidth => String::from("Switch Preset Column Widths"),
Action::MaximizeColumn => String::from("Maximize Column"),
Action::ConsumeWindowIntoColumn => String::from("Consume Window Into Column"),
Action::ExpelWindowFromColumn => String::from("Expel Window From Column"),
Action::Screenshot => String::from("Take a Screenshot"),
Action::Spawn(args) => format!(
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
args.first().unwrap_or(&String::new())
),
_ => String::from("FIXME: Unknown"),
}
}
fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
let mut name = String::new();
let has_comp_mod = key.modifiers.contains(Modifiers::COMPOSITOR);
if key.modifiers.contains(Modifiers::SUPER)
|| (has_comp_mod && comp_mod == CompositorMod::Super)
{
name.push_str("Super + ");
}
if key.modifiers.contains(Modifiers::ALT) || (has_comp_mod && comp_mod == CompositorMod::Alt) {
name.push_str("Alt + ");
}
if key.modifiers.contains(Modifiers::SHIFT) {
name.push_str("Shift + ");
}
if key.modifiers.contains(Modifiers::CTRL) {
name.push_str("Ctrl + ");
}
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
}
fn prettify_keysym_name(name: &str) -> String {
let name = match name {
"slash" => "/",
"comma" => ",",
"period" => ".",
"minus" => "-",
"equal" => "=",
"grave" => "`",
"Next" => "Page Down",
"Prior" => "Page Up",
"Print" => "PrtSc",
"Return" => "Enter",
_ => name,
};
if name.len() == 1 && name.is_ascii() {
name.to_ascii_uppercase()
} else {
name.into()
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod config_error_notification;
pub mod exit_confirm_dialog;
pub mod hotkey_overlay;
pub mod screenshot_ui;
@@ -5,6 +5,7 @@ use std::mem;
use anyhow::Context;
use arrayvec::ArrayVec;
use niri_config::Action;
use smithay::backend::allocator::Fourcc;
use smithay::backend::input::{ButtonState, MouseButton};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
@@ -14,10 +15,11 @@ use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::ExportMem;
use smithay::input::keyboard::{Keysym, ModifiersState};
use smithay::output::{Output, WeakOutput};
use smithay::render_elements;
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
use crate::config::Action;
use crate::niri_render_elements;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::RenderTarget;
const BORDER: i32 = 2;
@@ -39,17 +41,20 @@ pub enum ScreenshotUi {
pub struct OutputData {
size: Size<i32, Physical>,
texture: GlesTexture,
texture_buffer: TextureBuffer<GlesTexture>,
scale: i32,
transform: Transform,
// Output, screencast, screen capture.
texture: [GlesTexture; 3],
texture_buffer: [TextureBuffer<GlesTexture>; 3],
buffers: [SolidColorBuffer; 8],
locations: [Point<i32, Physical>; 8],
}
render_elements! {
#[derive(Debug)]
pub ScreenshotUiRenderElement<R>;
Screenshot = TextureRenderElement<R::TextureId>,
SolidColor = SolidColorRenderElement,
niri_render_elements! {
ScreenshotUiRenderElement => {
Screenshot = PrimaryGpuTextureRenderElement,
SolidColor = SolidColorRenderElement,
}
}
impl ScreenshotUi {
@@ -62,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() {
@@ -92,6 +98,7 @@ impl ScreenshotUi {
)
}
};
let scale = selection.0.current_scale().integer_scale();
let selection = (
selection.0,
@@ -102,16 +109,13 @@ impl ScreenshotUi {
let output_data = screenshots
.into_iter()
.map(|(output, texture)| {
let output_transform = output.current_transform();
let transform = output.current_transform();
let output_mode = output.current_mode().unwrap();
let size = output_transform.transform_size(output_mode.size);
let texture_buffer = TextureBuffer::from_texture(
renderer,
texture.clone(),
output.current_scale().integer_scale(),
Transform::Normal,
None,
);
let size = transform.transform_size(output_mode.size);
let scale = output.current_scale().integer_scale();
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.]),
@@ -125,6 +129,8 @@ impl ScreenshotUi {
let locations = [Default::default(); 8];
let data = OutputData {
size,
scale,
transform,
texture,
texture_buffer,
buffers,
@@ -239,7 +245,8 @@ impl ScreenshotUi {
pub fn render_output(
&self,
output: &Output,
) -> ArrayVec<ScreenshotUiRenderElement<GlesRenderer>, 9> {
target: RenderTarget,
) -> ArrayVec<ScreenshotUiRenderElement, 9> {
let _span = tracy_client::span!("ScreenshotUi::render_output");
let Self::Open { output_data, .. } = self else {
@@ -265,15 +272,20 @@ impl ScreenshotUi {
}));
// The screenshot itself goes last.
let index = match target {
RenderTarget::Output => 0,
RenderTarget::Screencast => 1,
RenderTarget::ScreenCapture => 2,
};
elements.push(
TextureRenderElement::from_texture_buffer(
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
(0., 0.),
&output_data.texture_buffer,
&output_data.texture_buffer[index],
None,
None,
None,
Kind::Unspecified,
)
))
.into(),
);
@@ -303,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)
@@ -312,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> {
@@ -332,9 +344,10 @@ impl ScreenshotUi {
}
}
pub fn output_size(&self, output: &Output) -> Option<Size<i32, Physical>> {
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
if let Self::Open { output_data, .. } = self {
Some(output_data.get(output)?.size)
let data = output_data.get(output)?;
Some((data.size, data.scale, data.transform))
} else {
None
}
+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()
}
}
+88 -88
View File
@@ -1,20 +1,32 @@
use std::ffi::{CString, OsStr};
use std::io::{self, Write};
use std::io::Write;
use std::os::unix::prelude::OsStrExt;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::path::{Path, PathBuf};
use std::ptr::null_mut;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use anyhow::{ensure, Context};
use directories::UserDirs;
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};
use smithay::utils::{Logical, Point, Rectangle, Size, Transform};
use crate::config::Config;
pub mod id;
pub mod spawning;
pub mod watcher;
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
pub fn version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
git_version!(fallback = "unknown commit"),
)
}
pub fn get_monotonic_time() -> Duration {
let ts = clock_gettime(ClockId::Monotonic);
@@ -25,6 +37,65 @@ 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();
let output_mode = output.current_mode().unwrap();
output_transform
.transform_size(output_mode.size)
.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")?;
Ok(Some([dirs.home_dir(), rest].iter().collect()))
} else {
Ok(None)
}
}
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
let Some(path) = &config.screenshot_path else {
return Ok(None);
@@ -47,91 +118,13 @@ pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>>
path = PathBuf::from(OsStr::from_bytes(&buf[..rv]));
}
if let Ok(rest) = path.strip_prefix("~") {
let dirs = UserDirs::new().context("error retrieving home directory")?;
path = [dirs.home_dir(), rest].iter().collect();
if let Some(expanded) = expand_home(&path).context("error expanding ~")? {
path = expanded;
}
Ok(Some(path))
}
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
/// Spawns the command to run independently of the compositor.
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
let _span = tracy_client::span!();
if command.is_empty() {
return;
}
// Spawning and waiting takes some milliseconds, so do it in a thread.
let res = thread::Builder::new()
.name("Command Spawner".to_owned())
.spawn(move || {
let (command, args) = command.split_first().unwrap();
spawn_sync(command, args);
});
if let Err(err) = res {
warn!("error spawning a thread to spawn the command: {err:?}");
}
}
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
let _span = tracy_client::span!();
let command = command.as_ref();
let mut process = Command::new(command);
process
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_BACKTRACE");
}
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_LIB_BACKTRACE");
}
// Double-fork to avoid having to waitpid the child.
unsafe {
process.pre_exec(|| {
match libc::fork() {
-1 => return Err(io::Error::last_os_error()),
0 => (),
_ => libc::_exit(0),
}
Ok(())
});
}
let mut child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return;
}
};
match child.wait() {
Ok(status) => {
if !status.success() {
warn!("child did not exit successfully: {status:?}");
}
}
Err(err) => {
warn!("error waiting for child: {err:?}");
}
}
}
pub fn write_png_rgba8(
w: impl Write,
width: u32,
@@ -176,3 +169,10 @@ pub fn show_screenshot_notification(image_path: Option<PathBuf>) {
warn!("error showing screenshot notification: {err:?}");
}
}
#[inline(never)]
pub fn cause_panic() {
let a = Duration::from_secs(1);
let b = Duration::from_secs(2);
let _ = a - b;
}
+378
View File
@@ -0,0 +1,378 @@
use std::ffi::OsStr;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use std::{io, thread};
use niri_config::Environment;
use crate::utils::expand_home;
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static CHILD_ENV: RwLock<Environment> = RwLock::new(Environment(Vec::new()));
/// Spawns the command to run independently of the compositor.
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
let _span = tracy_client::span!();
if command.is_empty() {
return;
}
// Spawning and waiting takes some milliseconds, so do it in a thread.
let res = thread::Builder::new()
.name("Command Spawner".to_owned())
.spawn(move || {
let (command, args) = command.split_first().unwrap();
spawn_sync(command, args);
});
if let Err(err) = res {
warn!("error spawning a thread to spawn the command: {err:?}");
}
}
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
let _span = tracy_client::span!();
let mut command = command.as_ref();
// Expand `~` at the start.
let expanded = expand_home(Path::new(command));
match &expanded {
Ok(Some(expanded)) => command = expanded.as_ref(),
Ok(None) => (),
Err(err) => {
warn!("error expanding ~: {err:?}");
}
}
let mut process = Command::new(command);
process
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_BACKTRACE");
}
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
process.env_remove("RUST_LIB_BACKTRACE");
}
// Set configured environment.
let env = CHILD_ENV.read().unwrap();
for var in &env.0 {
if let Some(value) = &var.value {
process.env(&var.name, value);
} else {
process.env_remove(&var.name);
}
}
drop(env);
let Some(mut child) = do_spawn(command, process) else {
return;
};
match child.wait() {
Ok(status) => {
if !status.success() {
warn!("child did not exit successfully: {status:?}");
}
}
Err(err) => {
warn!("error waiting for child: {err:?}");
}
}
}
#[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),
}
Ok(())
});
}
let child = match process.spawn() {
Ok(child) => child,
Err(err) => {
warn!("error spawning {command:?}: {err:?}");
return None;
}
};
Some(child)
}
#[cfg(feature = "systemd")]
use systemd::do_spawn;
#[cfg(feature = "systemd")]
mod systemd {
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
use smithay::reexports::rustix;
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
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)
}
#[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);
}
written += n;
if written == buf.len() {
return Ok(());
}
}
}
#[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);
}
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 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(())
}
}
+348
View File
@@ -0,0 +1,348 @@
//! File modification watcher.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc};
use std::thread;
use std::time::Duration;
use smithay::reexports::calloop::channel::SyncSender;
pub struct Watcher {
should_stop: Arc<AtomicBool>,
}
impl Drop for Watcher {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::SeqCst);
}
}
impl Watcher {
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
Self::with_start_notification(path, changed, None)
}
pub fn with_start_notification(
path: PathBuf,
changed: SyncSender<()>,
started: Option<mpsc::SyncSender<()>>,
) -> Self {
let should_stop = Arc::new(AtomicBool::new(false));
{
let should_stop = should_stop.clone();
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
// this "should" be as simple as mtime, but it does not quite work in practice;
// it doesn't work if the config is a symlink, and its target changes but the
// new target and old target have identical mtimes.
//
// in practice, this does not occur on any systems other than nix.
// because, on nix practically everything is a symlink to /nix/store
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
// so, symlink targets change frequently when mtime doesn't.
let mut last_props = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
.ok();
if let Some(started) = started {
let _ = started.send(());
}
loop {
thread::sleep(Duration::from_millis(500));
if should_stop.load(Ordering::SeqCst) {
break;
}
if let Ok(new_props) = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
{
if last_props.as_ref() != Some(&new_props) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
warn!("error sending change notification: {err:?}");
break;
}
last_props = Some(new_props);
}
}
}
debug!("exiting watcher thread for {}", path.to_string_lossy());
})
.unwrap();
}
Self { should_stop }
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::sync::atomic::AtomicU8;
use calloop::channel::sync_channel;
use calloop::EventLoop;
use smithay::reexports::rustix::fs::{futimens, Timestamps};
use smithay::reexports::rustix::time::Timespec;
use xshell::{cmd, Shell};
use super::*;
fn check(
setup: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
change: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
) {
let sh = Shell::new().unwrap();
let temp_dir = sh.create_temp_dir().unwrap();
sh.change_dir(temp_dir.path());
// let dir = sh.create_dir("xshell").unwrap();
// sh.change_dir(dir);
let mut config_path = sh.current_dir();
config_path.push("niri");
config_path.push("config.kdl");
setup(&sh).unwrap();
let changed = AtomicU8::new(0);
let mut event_loop = EventLoop::try_new().unwrap();
let loop_handle = event_loop.handle();
let (tx, rx) = sync_channel(1);
let (started_tx, started_rx) = mpsc::sync_channel(1);
let _watcher = Watcher::with_start_notification(config_path.clone(), tx, Some(started_tx));
loop_handle
.insert_source(rx, |_, _, _| {
changed.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
started_rx.recv().unwrap();
// HACK: if we don't sleep, files might have the same mtime.
thread::sleep(Duration::from_millis(100));
change(&sh).unwrap();
event_loop
.dispatch(Duration::from_millis(750), &mut ())
.unwrap();
assert_eq!(changed.load(Ordering::SeqCst), 1);
// Verify that the watcher didn't break.
sh.write_file(&config_path, "c").unwrap();
event_loop
.dispatch(Duration::from_millis(750), &mut ())
.unwrap();
assert_eq!(changed.load(Ordering::SeqCst), 2);
}
#[test]
fn change_file() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn create_file() {
check(
|sh| {
sh.create_dir("niri")?;
Ok(())
},
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
);
}
#[test]
fn create_dir_and_file() {
check(
|_sh| Ok(()),
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
);
}
#[test]
fn change_linked_file() {
check(
|sh| {
sh.write_file("niri/config2.kdl", "a")?;
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
Ok(())
},
|sh| {
sh.write_file("niri/config2.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn change_file_in_linked_dir() {
check(
|sh| {
sh.write_file("niri2/config.kdl", "a")?;
cmd!(sh, "ln -s niri2 niri").run()?;
Ok(())
},
|sh| {
sh.write_file("niri2/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn recreate_file() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.remove_path("niri/config.kdl")?;
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn recreate_dir() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.remove_path("niri")?;
sh.write_file("niri/config.kdl", "b")?;
Ok(())
},
);
}
#[test]
fn swap_dir() {
check(
|sh| {
sh.write_file("niri/config.kdl", "a")?;
Ok(())
},
|sh| {
sh.write_file("niri2/config.kdl", "b")?;
sh.remove_path("niri")?;
cmd!(sh, "mv niri2 niri").run()?;
Ok(())
},
);
}
#[test]
fn swap_just_link() {
// NixOS setup: link path changes, mtime stays constant.
check(
|sh| {
let mut dir = sh.current_dir();
dir.push("niri");
sh.create_dir(&dir)?;
let mut d2 = dir.clone();
d2.push("config2.kdl");
let mut c2 = File::create(d2).unwrap();
write!(c2, "a")?;
c2.flush()?;
futimens(
&c2,
&Timestamps {
last_access: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
last_modification: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
},
)?;
c2.sync_all()?;
drop(c2);
let mut d3 = dir.clone();
d3.push("config3.kdl");
let mut c3 = File::create(d3).unwrap();
write!(c3, "b")?;
c3.flush()?;
futimens(
&c3,
&Timestamps {
last_access: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
last_modification: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
},
)?;
c3.sync_all()?;
drop(c3);
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
Ok(())
},
|sh| {
cmd!(sh, "unlink niri/config.kdl").run()?;
cmd!(sh, "ln -s config3.kdl niri/config.kdl").run()?;
Ok(())
},
);
}
#[test]
fn swap_dir_link() {
check(
|sh| {
sh.write_file("niri2/config.kdl", "a")?;
cmd!(sh, "ln -s niri2 niri").run()?;
Ok(())
},
|sh| {
sh.write_file("niri3/config.kdl", "b")?;
cmd!(sh, "unlink niri").run()?;
cmd!(sh, "ln -s niri3 niri").run()?;
Ok(())
},
);
}
}
-60
View File
@@ -1,60 +0,0 @@
//! File modification watcher.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use smithay::reexports::calloop::channel::SyncSender;
pub struct Watcher {
should_stop: Arc<AtomicBool>,
}
impl Drop for Watcher {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::SeqCst);
}
}
impl Watcher {
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
let should_stop = Arc::new(AtomicBool::new(false));
{
let should_stop = should_stop.clone();
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
loop {
thread::sleep(Duration::from_millis(500));
if should_stop.load(Ordering::SeqCst) {
break;
}
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
if last_mtime != Some(mtime) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
warn!("error sending change notification: {err:?}");
break;
}
last_mtime = Some(mtime);
}
}
}
debug!("exiting watcher thread for {}", path.to_string_lossy());
})
.unwrap();
}
Self { should_stop }
}
}
+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
}
+66
View File
@@ -0,0 +1,66 @@
use smithay::desktop::Window;
use smithay::output::Output;
use smithay::wayland::shell::xdg::ToplevelSurface;
use super::ResolvedWindowRules;
use crate::layout::workspace::ColumnWidth;
#[derive(Debug)]
pub struct Unmapped {
pub window: Window,
pub state: InitialConfigureState,
}
#[derive(Debug)]
pub enum InitialConfigureState {
/// The window has not been initially configured yet.
NotConfigured {
/// Whether the window requested to be fullscreened, and the requested output, if any.
wants_fullscreen: Option<Option<Output>>,
},
/// The window has been configured.
Configured {
/// Up-to-date rules.
///
/// We start tracking window rules when sending the initial configure, since they don't
/// affect anything before that.
rules: ResolvedWindowRules,
/// Resolved default width for this window.
///
/// `None` means that the window will pick its own width.
width: Option<ColumnWidth>,
/// Whether the window should open full-width.
is_full_width: bool,
/// Output to open this window on.
///
/// This can be `None` in cases like:
///
/// - There are no outputs connected.
/// - This is a dialog with a parent, and there was no explicit output set, so this dialog
/// should fetch the parent's current output again upon mapping.
output: Option<Output>,
},
}
impl Unmapped {
/// Wraps a newly created window that hasn't been initially configured yet.
pub fn new(window: Window) -> Self {
Self {
window,
state: InitialConfigureState::NotConfigured {
wants_fullscreen: None,
},
}
}
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/

Some files were not shown because too many files have changed in this diff Show More