Niri Plus / Desktop guide
Common desktop utilities
Network, Bluetooth, Audio and Session are available from the regular Rofi
application list: use Mod+D and search for the displayed name. The current
managed desktop uses direct Rofi controls for the first
three and the shared Session helper for locking, logout and power actions.
Components 0.4.3 are installed on Carbon, Starship and Snap; the
managed setup record keeps installed checks and
physical-device acceptance separate.
This page retains the earlier nmtui, bluetoothctl and pavucontrol launchers
as a simple recovery option. It also owns the conditional network handover and
shared Session behavior used by the main guide. Follow the main agent guide
for current direct-control installation. Do not install the three legacy
entries over the direct entries: they deliberately use the same desktop IDs.
Retained entry requirements
| Entry | Required software and readiness |
|---|---|
| Network | kitty, NetworkManager, and its nmtui text interface. On Arch, nmtui is shipped by the networkmanager package. NetworkManager must own the connection devices. Package installation alone does not hand off a network link; use the conditional handover only for the documented cloud-init/networkd starting state. |
| Bluetooth | kitty, bluez, bluez-utils for bluetoothctl, a working system D-Bus, and the packaged bluetooth.service. A Bluetooth adapter must be present for radio and pairing operations. The separate Arch bluetuith terminal UI may also be installed; the reference menu entry intentionally opens bluetoothctl. |
| Audio | pavucontrol and a running PulseAudio-compatible server, such as PipeWire's PulseAudio server or PulseAudio. |
| Session | Python 3.11 or later, rofi, systemd user services, niri for native logout, and an active Wayland socket. Install the independent manual lock service before using Lock. |
Removable-drive mounting through a file manager has a separate prerequisite:
install UDisks2 (udisks2 on Arch), keep its service and polkit authorization
available, and install the filesystem support for the media you use. UDisks2
is not invoked by these four entries.
Retained portable files
- Network entry runs
kitty -e nmtui. - Bluetooth entry runs
kitty -e bluetoothctl. - Audio entry opens
pavucontrol. - Session entry opens the small Session helper.
- Wayland readiness check tests whether the current
WAYLAND_DISPLAYsocket accepts a local connection. - Power-policy example makes lock-before-suspend an explicit Boolean.
The readiness check confirms a responding Wayland socket, not which compositor
owns it. Launch Session from Niri; niri msg action quit is the compositor's
own exit request and will fail safely if Niri is not reachable. Ensure
~/.local/bin is in the graphical session's PATH so Rofi can find
desktop-session-menu and its adjacent desktop-session-ready helper.
The opt-in native session screen
uses desktop-session-menu --screen and the adjacent desktop-suspend helper.
It provides a six-action grid. Selecting an action starts its cancellable
countdown, which is the confirmation before automatic execution. The default
launcher invocation above keeps the reference's legacy
menu; select the screen's portable Ctrl+Alt+Delete profile intentionally.
The Niri starter binds Mod+D to rofi -show drun, which lists these entries;
the entries themselves do not need separate key bindings.
Optional Close All
Published components 0.10.0 add the first-party
niri-close-all utility and its
opt-in Niri profile.
It requests ordinary closes for all normal windows across every workspace
and output in the captured Niri session. It is not limited to the focused
workspace, output or application.
The helper reads niri msg --json windows once, validates and deduplicates its
unsigned window IDs, then sends one niri msg action close-window --id ID
request per captured ID. Niri's window IPC
describes toplevel windows; layer-shell panels are separate surfaces. Floating
and unfocused windows are included. Additional IPC fields are ignored, but an
invalid ID rejects the entire snapshot before any close request is sent.
Applications keep their normal save/cancel dialogs and may remain open. New dialogs and windows created after the snapshot are not added to the operation. The helper does not kill applications, quit the session, focus each window, or repeatedly query until the desktop is empty. A disappeared or refused ID is attempted once without retry; other captured IDs are still attempted. The helper checks the captured compositor socket before each request and stops if that socket changes or disappears.
Use Python 3.11+ and a Niri session exposing NIRI_SOCKET. For a reviewed
components-v0.10.0 checkout, first inspect existing target and binding
preimages. Install only the helper, and merge or include the optional profile:
install -Dm755 examples/desktop/niri-close-all "$HOME/.local/bin/niri-close-all"
Mod+Shift+Q repeat=false hotkey-overlay-title="Close all windows (all workspaces and outputs)" { spawn "niri-close-all"; }
Keep Mod+Q { close-window; } unchanged. The portable starter does not select
Close All automatically. This action dispatches immediately when invoked;
it is separate from the session screen's six-action countdown. No hot-corner
tool, startup service or additional privilege is required.
For read-only inspection, niri-close-all --dry-run prints the one captured
ID list without sending closes. After a valid snapshot, both modes print a JSON record with scope,
dry_run, snapshot_ids, requested_ids, failed_ids and
not_attempted_ids. requested_ids means Niri accepted the request, not that
an application actually closed. Exit status is 0 when the snapshot and every
attempted request succeed (including an empty desktop), 1 on snapshot/IPC or
partial request failure, and 2 for invalid command-line arguments.
An invalid or unavailable snapshot reports an error on stderr without sending
any close request.
Managed consumers must use the published immutable archive, verify its manifest, and select only the helper/profile alongside their existing pins. The qualified reference is not updated by this optional release. Tests use private IPC command fixtures; never test it by closing ordinary live work. Removing the optional binding and helper reverses adoption; applications already closed by an explicit invocation cannot be reopened by that rollback.
Network owner handover
NetworkManager is installed by the main walkthrough, but installation does not
start it or transfer a connection. If NetworkManager already owns the active
link, keep it and skip this section. This example applies only when
systemd-networkd owns eth0, /etc/systemd/network/10-cloud-init-eth0.network
is the exact active rule, cloud-init currently configures networking, and
/etc/resolv.conf is already a symlink to the resolver stub. If any owner,
path, or resolver differs, keep the existing owner and review a machine-specific
migration; do not use this sample as a general network-manager swap.
Keep a local console available and leave your current recovery connection
untouched. The rollback timer below is armed first and re-enables networkd after
120 seconds unless you cancel it after proving the new path. The scripts live
in root-owned /run, so the transient rollback stays available for this boot
without becoming another installed service:
if sudo test -e /run/niri-plus-guide || sudo test -L /run/niri-plus-guide; then
printf '%s\n' 'Inspect existing /run/niri-plus-guide before continuing.' >&2
exit 1
fi
sudo install -d -o root -g root -m700 /run/niri-plus-guide
sudo tee /run/niri-plus-guide/network-rollback.sh >/dev/null <<'EOF'
#!/usr/bin/bash
set -euo pipefail
network_file=/etc/systemd/network/10-cloud-init-eth0.network
disabled_file="$network_file.reference-disabled"
nm_conf=/etc/NetworkManager/conf.d/90-niri-plus-reference.conf
cloud_conf=/etc/cloud/cloud.cfg.d/99-niri-plus-reference-network.cfg
expected_nm=$'[main]\ndns=systemd-resolved'
expected_cloud='network: {config: disabled}'
check_owned_file() {
local path="$1" expected="$2" current
[[ ! -L "$path" ]] || { echo "refusing symlink: $path" >&2; exit 1; }
if [[ -e "$path" ]]; then
current="$(cat -- "$path")"
[[ "$current" == "$expected" ]] || {
echo "refusing changed file: $path" >&2
exit 1
}
fi
}
check_owned_file "$nm_conf" "$expected_nm"
check_owned_file "$cloud_conf" "$expected_cloud"
if [[ -e "$network_file" && -e "$disabled_file" ]]; then
echo "both network rule paths exist; inspect before recovery" >&2
exit 1
fi
if [[ ! -e "$network_file" && ! -f "$disabled_file" ]]; then
echo "network rule preimage is missing" >&2
exit 1
fi
[[ ! -L "$network_file" && ! -L "$disabled_file" ]] || {
echo "refusing network-rule symlink" >&2
exit 1
}
systemctl disable --now NetworkManager.service
rm -f -- "$nm_conf" "$cloud_conf"
if [[ ! -e "$network_file" ]]; then
mv -- "$disabled_file" "$network_file"
fi
systemctl enable --now \
systemd-networkd.service systemd-networkd-wait-online.service \
systemd-networkd.socket systemd-networkd-varlink.socket \
systemd-networkd-varlink-metrics.socket systemd-networkd-resolve-hook.socket
EOF
sudo chmod 700 /run/niri-plus-guide/network-rollback.sh
sudo tee /run/niri-plus-guide/network-handover.sh >/dev/null <<'EOF'
#!/usr/bin/bash
set -euo pipefail
network_file=/etc/systemd/network/10-cloud-init-eth0.network
disabled_file="$network_file.reference-disabled"
nm_conf=/etc/NetworkManager/conf.d/90-niri-plus-reference.conf
cloud_conf=/etc/cloud/cloud.cfg.d/99-niri-plus-reference-network.cfg
[[ -f "$network_file" && ! -L "$network_file" ]] || {
echo "expected cloud-init networkd rule is absent or not a regular file" >&2
exit 1
}
[[ ! -e "$disabled_file" && ! -L "$disabled_file" ]] || {
echo "rollback preimage path already exists" >&2
exit 1
}
[[ ! -e "$nm_conf" && ! -L "$nm_conf" ]] || {
echo "inspect existing NetworkManager config before continuing" >&2
exit 1
}
[[ ! -e "$cloud_conf" && ! -L "$cloud_conf" ]] || {
echo "inspect existing cloud-init config before continuing" >&2
exit 1
}
systemctl is-active --quiet systemd-networkd.service || {
echo "systemd-networkd is not the active owner" >&2
exit 1
}
systemctl is-active --quiet systemd-resolved.service || {
echo "systemd-resolved is not active" >&2
exit 1
}
systemctl is-active --quiet NetworkManager.service && {
echo "NetworkManager is already active; skip this handover" >&2
exit 1
}
[[ -L /etc/resolv.conf ]] || {
echo "/etc/resolv.conf is not a symlink; inspect before continuing" >&2
exit 1
}
install -d /etc/NetworkManager/conf.d /etc/cloud/cloud.cfg.d
printf '[main]\ndns=systemd-resolved\n' > "$nm_conf"
printf 'network: {config: disabled}\n' > "$cloud_conf"
mv -- "$network_file" "$disabled_file"
systemctl disable --now \
systemd-networkd.socket systemd-networkd-varlink.socket \
systemd-networkd-varlink-metrics.socket systemd-networkd-resolve-hook.socket
systemctl disable --now systemd-networkd.service systemd-networkd-wait-online.service
systemctl enable --now NetworkManager.service
EOF
sudo chmod 700 /run/niri-plus-guide/network-handover.sh
Inspect the exact active rule and resolver link before scheduling anything. Do not overwrite pre-existing NetworkManager or cloud-init files. Then arm the rollback and run the handover through transient system units:
sudo sed -n '1,160p' /etc/systemd/network/10-cloud-init-eth0.network
readlink /etc/resolv.conf
sudo systemd-run --unit=niri-plus-network-rollback --on-active=120s \
/run/niri-plus-guide/network-rollback.sh
sudo systemctl status niri-plus-network-rollback.timer
sudo systemd-run --wait --unit=niri-plus-network-handover \
/run/niri-plus-guide/network-handover.sh
Verify the existing address stayed attached, NetworkManager has an active connection, networkd and all four activation sockets are stopped, and DNS plus HTTPS work before the timer expires:
nmcli -g GENERAL.STATE,GENERAL.CONNECTION device show eth0
nmcli -t -f NAME,TYPE,DEVICE connection show --active
ip -4 address show dev eth0
systemctl is-active NetworkManager.service systemd-resolved.service
systemctl is-active \
systemd-networkd.service systemd-networkd-wait-online.service \
systemd-networkd.socket systemd-networkd-varlink.socket \
systemd-networkd-varlink-metrics.socket systemd-networkd-resolve-hook.socket || true
readlink /etc/resolv.conf
getent ahosts archlinux.org
curl --fail --silent --show-error --head https://archlinux.org/
If all checks pass, cancel the rollback before its 120-second deadline and confirm the timer stopped:
sudo systemctl stop niri-plus-network-rollback.timer
sudo systemctl is-active niri-plus-network-rollback.timer || true
If the link or checks fail, do not stop the timer; let systemd restore networkd. If the timer has already failed or the console is still available, inspect the transient-unit logs and invoke the same root-owned rollback script manually:
sudo journalctl -b -u niri-plus-network-handover.service \
-u niri-plus-network-rollback.service --no-pager
sudo /run/niri-plus-guide/network-rollback.sh
After a successful handover, direct Network controls can observe the native
connection; nmtui remains available as a recovery tool.
The change is local to the selected owner and cloud-init rule; it does not
configure another machine's networking or alter unrelated interfaces.
Install and choose policy
For the legacy launcher option only, use the reviewed repository checkout.
The current 0.4.3 staged SOURCE intentionally omits the three old device
entries. In the main walkthrough, NIRI_PLUS_CHECKOUT names that checkout.
These commands overwrite the four listed desktop IDs, so inspect existing
entries and keep their preimages before choosing this alternative:
LEGACY_SOURCE="$NIRI_PLUS_CHECKOUT"
test -f "$LEGACY_SOURCE/examples/desktop/niri-plus-network.desktop"
install -Dm755 "$LEGACY_SOURCE/examples/desktop/desktop-session-ready" "$HOME/.local/bin/desktop-session-ready"
install -Dm755 "$LEGACY_SOURCE/examples/desktop/desktop-session-menu" "$HOME/.local/bin/desktop-session-menu"
install -Dm644 "$LEGACY_SOURCE/examples/desktop/niri-plus-network.desktop" "$HOME/.local/share/applications/niri-plus-network.desktop"
install -Dm644 "$LEGACY_SOURCE/examples/desktop/niri-plus-bluetooth.desktop" "$HOME/.local/share/applications/niri-plus-bluetooth.desktop"
install -Dm644 "$LEGACY_SOURCE/examples/desktop/niri-plus-audio.desktop" "$HOME/.local/share/applications/niri-plus-audio.desktop"
install -Dm644 "$LEGACY_SOURCE/examples/desktop/niri-plus-session.desktop" "$HOME/.local/share/applications/niri-plus-session.desktop"
Choose your own power setting before copying the sample to
${XDG_CONFIG_HOME:-$HOME/.config}/desktop-session/power.toml. The sample
contains lock_before_suspend = false, matching the value in the managed
source record captured for its three current hosts. Keep or change it only to
match your chosen policy. The Session helper refuses to suspend when the file
is missing, malformed, or lacks a Boolean value; it never guesses.
The sample configuration is:
lock_before_suspend = false
Session behavior
The table below describes the retained native behavior without optional confirmation adoption. Full-screen manual confirmation can route Lock, display-off, Suspend, Restart and Shut down through one shared five-second, Escape-cancellable overlay. Its host policy is independent of the automatic idle policy; Log out retains Niri's own confirmation.
| Choice | Dispatch |
|---|---|
| Lock | Starts the independent desktop-lock.service, so policy reloads do not stop a manual lock. |
| Log out | Runs niri msg action quit and preserves Niri's native confirmation. |
| Suspend | If configured, starts the lock service first; then requests systemctl suspend. A failed lock request prevents suspend. |
| Restart | Shows a second Rofi picker with Cancel selected by default; Restart dispatches systemctl reboot. |
| Shut down | Shows the same confirmation pattern; Shut down dispatches systemctl poweroff. |
| Cancel / Escape | Returns without dispatching an action. |
The retained Rofi route does not use --skip-confirmation, directly start a locker, restart
Niri, or change the existing idle/lid policy. The desktop-lock.service keeps
manual locking independent from idle behavior. On battery and desktop systems,
configure suspend, lock-before-suspend, and lid behavior together in the
separately owned session policy.
Pairing with Bluetooth
The Bluetooth entry opens bluetoothctl in Kitty. Installing BlueZ does not
start its daemon. When an adapter is present and you are ready to use it, start
the packaged service; enable it only if you want it available after each boot.
Do not start a second bluetoothd process:
sudo systemctl start bluetooth.service
systemctl status bluetooth.service
bluetoothctl list
If no controller is listed, stop here and check that the host exposes an adapter. Arch's Bluetooth guide lists the daemon and utility packages and service readiness:
bluetoothctl
power on
agent on
default-agent
scan on
When the desired device appears, use its address with pair, then trust and
connect if appropriate. Stop scanning with scan off when finished. Device
prompts and available pairing methods vary; accept only the pairing request for
the device you intend to connect.
Backlight and external displays
Laptop brightness keys and their replaceable notification feedback control the laptop panel's backlight device. External monitors do not necessarily expose that device. Monitor brightness through DDC/CI or physical monitor buttons is a separate capability and is not controlled by these application entries.
The helper's cancellation and dispatch path has isolated checks in
scripts/check-terminal.py. The checks use
stubs and do not toggle radios, change audio routing, suspend, restart, power
off, or lock a live desktop.