A terminal-based screensaver for Niri, driven by TerminalTextEffects and designed to integrate with the Noctalia desktop shell.
8-second loop on CachyOS — the niri brand mark rendered with one of TTE's particle/rain effects on the cyan-to-magenta gradient.
Forked from cosmic-order's screensaver component, with the COSMIC-specific glue (cosmic-randr, cosmic-greeter, the focus-follows-cursor / autotile dance) stripped out and replaced with niri-native equivalents. Idle, lock, and DPMS are deferred to Noctalia rather than reimplemented in swayidle.
bin/
niri-screensaver Inner driver — runs TTE in the current terminal.
niri-screensaver-launch Spawns one fullscreen Alacritty per output, runs the driver inside.
niri-screensaver-ctl Thin shim: launch | kill | toggle | status | test | preview | effects.
share/
alacritty-screensaver.toml Minimal Alacritty config (black bg, no padding, hidden cursor).
logos/ ASCII art logos (Framework cog, CachyOS shield, combos). See `Logos` below.
docs/
niri-window-rule.kdl Snippet for ~/.config/niri/config.kdl.
noctalia-customCommand.json Snippet for ~/.config/noctalia/settings.json idle.customCommands.
noctalia-plugin/ Native Noctalia plugin (manifest + QML).
install.sh User-local install (defaults to ~/.local).
| Package | Required | Purpose |
|---|---|---|
niri |
yes | The compositor; window-rule + niri msg action spawn |
alacritty |
yes | Host terminal for the fullscreen screensaver surface |
terminaltexteffects (tte) |
yes | Renders the actual effects |
jq |
optional | Used by the launcher to enumerate outputs |
figlet |
optional | Renders the between-effects clock and now-playing overlay |
playerctl |
optional | Source for the now-playing track display |
notify-send (libnotify) |
optional | Toggle / status notifications |
Install the dependencies for your distro:
# Arch / CachyOS
paru -S python-terminaltexteffects alacritty niri jq figlet libnotify playerctl
# Fedora / RHEL
sudo dnf install alacritty niri jq figlet libnotify playerctl
pipx install terminaltexteffects
# Debian / Ubuntu
sudo apt install alacritty jq figlet libnotify-bin playerctl
pipx install terminaltexteffects
# (niri may need a manual install on older releases)The Noctalia plugin additionally requires Noctalia ≥ 4.7.0 (uses the plugin
API's tr() translation helper and Tabler icon names).
yay -S niri-screensaver # stable, tracks tagged releases
# or
yay -S niri-screensaver-git # tracks main HEADSubstitute paru / pikaur / your AUR helper of choice. Either package
installs the bash CLI to /usr/bin, shared assets to
/usr/share/niri-screensaver/, the .desktop entry and hicolor icon
to the standard XDG paths, and prints a post-install message with the
remaining wire-up steps (niri window-rule, Noctalia plugin symlink).
The two packages provides/conflicts each other — install one or the
other, not both.
./install.sh # installs into ~/.local
INSTALL_PREFIX=/usr/local ./install.sh # system-wideThis deploys the three bin/ scripts and the share/ assets (Alacritty
config + logos + .desktop entry + hicolor icon). It does not
install the niri window-rule or the Noctalia plugin — those are separate
steps below.
niri-screensaver-ctl status
niri-screensaver-ctl test # render one effect inline (no fullscreen)Append the contents of docs/niri-window-rule.kdl to your ~/.config/niri/config.kdl.
The rule matches app-id="niri-screensaver" and applies open-fullscreen true,
which is how the launcher achieves fullscreen without an Alacritty CLI flag.
Three options, from least to most friction.
The plugin adds a Settings tab, a bar widget, and auto-registers the
screensaver in Noctalia's IdleService when enabled.
Niri logo mid-gradient with the Noctalia bar visible at the top — the plugin's bar widget (custom monitor-with-image icon, far left of the tray cluster) launches the screensaver on click.
If you installed via the AUR package above, the plugin source is already
on disk at /usr/share/niri-screensaver/noctalia-plugin/. Copy it into
Noctalia's per-user plugin dir:
mkdir -p ~/.config/noctalia/plugins
cp -r /usr/share/niri-screensaver/noctalia-plugin \
~/.config/noctalia/plugins/niri-screensaverThen enable it in Noctalia → Settings → Plugins.
Why
cp -rand notln -sfn— Noctalia writes plugin settings back into the plugin dir on every toggle in the Settings tab. The AUR-shipped tree under/usr/share/is root-owned, so a symlinked plugin loads fine but silently fails to persist any user-changed settings (defaults always come back on Noctalia restart). The cost of usingcp -ris that plugin updates frompacman -Syudon't auto-flow — re-run thecp -rafter each upgrade if you want the latest plugin code.
Pending registry acceptance. Until noctalia-plugins#852 merges,
niri-screensaverwon't appear in the registry browser yet — use Option A (AUR) or Option C below in the meantime.
Open Noctalia → Settings → Plugins, find niri-screensaver in the
registry browser, install. The plugin lands in
~/.config/noctalia/plugins/.
Note: the registry ships only the plugin's QML files. The bash CLI
(niri-screensaver-launch and friends) is a separate install — use the
AUR package on Arch / CachyOS, or ./install.sh from this repo on other
distros. If the CLI is missing, the plugin's Settings tab shows a
"install niri-screensaver first" banner.
If you don't want the Noctalia plugin (no Settings UI, no bar widget),
just wire the idle trigger directly: copy the relevant fields from
docs/noctalia-customCommand.json into ~/.config/noctalia/settings.json
under the idle object. After saving, restart Noctalia (pkill qs then
re-launch qs -c noctalia-shell) to pick up the new idle hook.
niri-screensaver-ctl launch # trigger now
niri-screensaver-ctl kill # stop
niri-screensaver-ctl status # report state
niri-screensaver-ctl toggle # disable / re-enable the launcher
niri-screensaver-ctl is-running # exit 0 if running, 1 if not (quiet; for scripts)
niri-screensaver-ctl test # run a single random effect inline (no fullscreen)
niri-screensaver-ctl preview rain # preview a specific named effect inline
niri-screensaver-ctl effects # list all TTE effectsThe launcher covers every output with its own fullscreen surface, pinning
each by window id so placement is correct even under niri's
focus-follows-mouse. Dismissing any one screen (key or mouse) wakes all of
them.
By default each output runs independently — a different random effect per
screen. Set MULTI_MONITOR_MODE=mirror (config, or the plugin's Multi-monitor
mode dropdown) to show the same effect on every output instead. Mirroring
is exact on monitors of the same resolution; on mismatched resolutions the same
effect plays but the layout differs (the canvas tracks each terminal's size).
~/.config/niri-screensaver/config is sourced as shell. Keys:
| Key | Default | Notes |
|---|---|---|
BATTERY_MIN_PERCENT |
0 |
Read by the launcher: skip auto-launch when on battery below this %. 0 disables; plugged in or no battery never skips; launch force overrides |
FRAME_RATE |
60 |
TTE frame rate |
INCLUDE_EFFECTS |
empty | Comma-separated effect names; takes precedence over excludes |
EXCLUDE_EFFECTS |
dev_worm |
Comma-separated effects to skip |
FADE_IN_EFFECT |
empty | One-shot effect on launch (e.g. expand, slide) |
FADE_OUT_EFFECT |
empty | One-shot effect on dismiss (e.g. burn, crumble) |
SHOW_CLOCK |
false |
Render time between effects |
CLOCK_DURATION |
3 |
Seconds to display the clock |
CLOCK_FORMAT |
%H:%M |
strftime format string |
CLOCK_FONT |
empty | figlet font name (shared with the now-playing overlay) |
SHOW_NOW_PLAYING |
false |
Render the playerctl track title between effects (no-op if playerctl is missing or nothing is playing) |
NOW_PLAYING_DURATION |
3 |
Seconds to display the now-playing overlay |
CURSOR_HIDE |
true |
Hide the text cursor (tput civis) |
DISMISS_ON_KEY |
true |
Any key dismisses; ESC and mouse always dismiss |
RANDOM_LOGO |
false |
When true, pick a random *.txt from LOGO_DIR before each effect cycle |
LOGO_DIR |
empty | Directory the random picker scans. Defaults to the installed share/logos/ |
MULTI_MONITOR_MODE |
independent |
independent = each output randomizes its own effect; mirror = all outputs share one seed (and one logo when RANDOM_LOGO is set) so they show the same effect. Mirror is most coherent on matched-resolution monitors |
MIRROR_INTERVAL |
8 |
Mirror mode only: seconds per effect window. All monitors switch effect on the same wall-clock boundary; lower = faster changes |
On launch, the launcher parks the mouse pointer in the bottom-right corner via
wlrctl (preferred) or ydotool if either is installed. For a full hide,
combine with niri's cursor { hide-after-inactive-ms 500 } so the parked
pointer disappears after the idle window. With neither tool installed, the
launcher logs a one-time hint and falls back to niri-only auto-hide.
share/logos/ ships ready-to-use ASCII art. Point LOGO_FILE at one of them
in ~/.config/niri-screensaver/config (or via the Noctalia plugin's Settings
panel) — or symlink your favorite to the active path:
ln -sf ~/.local/share/niri-screensaver/logos/framework-name-with-icon-medium.txt \
~/.config/niri-screensaver/logo.txt| File | Contents |
|---|---|
cachyos-icon.txt |
CachyOS shield |
cachyos-name.txt |
CACHYOS ANSI Shadow wordmark |
cachyos-name-with-icon.txt |
Shield + wordmark |
| File | Contents |
|---|---|
framework-icon.txt |
8-lobed Framework cog (40×18) |
framework-icon-medium.txt |
Same cog (30×14) |
framework-icon-small.txt |
Same cog (24×10) |
framework-name.txt |
FRAMEWORK ANSI Shadow wordmark |
framework-name-with-icon.txt |
Cog (40×18) + wordmark |
framework-name-with-icon-medium.txt |
Cog (30×14) + wordmark |
framework-name-with-icon-small.txt |
Cog (24×10) + wordmark |
framework-name-with-cachyos-icon.txt |
CachyOS shield + FRAMEWORK wordmark — for CachyOS-on-Framework setups |
| File | Contents |
|---|---|
hyprland-icon.txt |
Hyprland teardrop |
hyprland-name.txt |
HYPRLAND ANSI Shadow wordmark |
hyprland-name-with-icon.txt |
Teardrop + wordmark |
| File | Contents |
|---|---|
niri-icon.txt |
Stylized "i" / arguably owl-shaped niri brand mark |
niri-name.txt |
NIRI ANSI Shadow wordmark |
niri-name-with-icon.txt |
Icon + wordmark |
niri-tiles.txt |
Five scrolling-tile columns — niri's signature layout |
niri-name-with-tiles.txt |
Tiles + wordmark |
Per-file attribution, licensing, and trademark notes are in share/logos/LICENSES.md.
Drop any UTF-8 text file into ~/.local/share/niri-screensaver/logos/
(or share/logos/ in the repo) and point LOGO_FILE at it — or pick
it from the Noctalia plugin's logo dropdown (which auto-refreshes when
files appear in that directory). The plugin's Logo directory field also
has a Browse button if you want to point it at a different folder of
.txt files.
Size. Logos render as-is, no rescaling, so plan for the narrowest
terminal you'll run on. The shipped logos stay within ~40–60 columns
wide; go much wider and lines will wrap on smaller monitors. Height is
forgiving — niri-name-with-icon.txt is 49 lines and renders fine at
1080p. Useful reference points:
| Logo file | Width × height |
|---|---|
framework-icon-small.txt |
24 × 13 |
framework-icon.txt |
40 × 21 |
niri-name-with-icon.txt |
40 × 49 |
cachyos-name.txt |
60 × 9 |
Layout. TTE centers the entire block (the full bounding box of your file) horizontally and vertically on the output. A few consequences:
- Trailing whitespace counts. Lines padded with extra spaces on the
right widen the bounding box and shift the visual center off-axis.
Strip trailing whitespace before saving — most editors have a setting
for it (
:set listin vim, "Trim trailing whitespace on save" in VS Code). - Blank lines at top/bottom add vertical padding. Useful if you want breathing room around an icon-only logo. They're treated as part of the bounding box.
- Combining icon + wordmark (the
*-name-with-icon.txtpattern): stack them in one file with one or two blank lines between. They'll render as one block.
Characters. Block elements (█ ▓ ▒ ░) and box-drawing
(╔═╗ ║ ╚═╝) render most cleanly across monospace fonts. Per-glyph
ANSI Shadow wordmarks (the style used by every *-name.txt) are the
easiest way to get a polished result.
Tools.
- Wordmarks from text:
figlet -f "ANSI Shadow" YOURTEXT. If the font isn't installed, grab it from xero/figlet-fonts or use the web generator below. - Image → ASCII:
jp2a --width=40 --chars=" ░▒▓█" logo.pngorchafa --symbols=block --size=40x logo.png. Both emit pure UTF-8 without ANSI color codes (TTE won't preserve mid-effect colors). - Web generator: patorjk.com/software/taag/
has a font picker and a "Copy" button — paste the output into a
.txtand trim any trailing blank lines.
Preview. Render a single effect inline (no fullscreen) with your in-progress file:
LOGO_FILE=~/Downloads/mylogo.txt niri-screensaver-ctl test
LOGO_FILE=~/Downloads/mylogo.txt niri-screensaver-ctl test beamsIterate on the file, re-run test, drop it into the logos directory
when you're happy with it.
niri-screensaver is not affiliated with or endorsed by Framework
Computer Inc., the CachyOS project, the Hyprland project, or the niri
project. Brand marks rendered in share/logos/ belong to their
respective owners and are referenced for the convenience of users who
own / run those products. The Hyprland-derived ASCII carries the
upstream BSD-3-Clause attribution; Framework and CachyOS marks are
provided for nominative use only. See
share/logos/LICENSES.md for per-file detail.
If you are a brand owner and would like a logo removed or the attribution adjusted, please open an issue.
- The 1500-line
screensaver-ctl.sh(swayidle config generator, systemd unit installer, lock command setup) — Noctalia owns idle/lock/DPMS now. - The
disable_compositor_interferenceblock (focus_follows_cursor / autotile poking) — niri doesn't have the focus-stealing problem. - The
ydotoolSuper+F injection to toggle fullscreen — replaced by niri window-rule withopen-fullscreen true. - Ghostty-specific config generation; replaced with a single Alacritty TOML.
- cosmic-randr monitor enumeration; replaced with
niri msg --json outputs. cosmic-greeter --lock; Noctalia's native lock is invoked vialoginctl lock-sessionor directly through Noctalia's IdleService.- Power-aware effect profiles (UPower D-Bus). Add back via the Noctalia plugin if you want them.
GPL-3.0-only (carried over from cosmic-order).

