Niri Plus / Desktop guide
Neutral Slate appearance files
These are appearance-only examples for Neutral Slate with Clear Blue accents.
The Rofi, SwayNC, Gtklock, Qt, KDE, Niri, and Kitty exports are generated by
the public schema-2 renderer in components/appearance/render.py. Each
manifest entry records the identity SHA-256 and renderer provenance. The
renderer also returns shared role and font JSON for adapters that need the
same values without generating their own palette. These are public examples,
not proof of native installation or deployment.
To stage all renderer outputs for review, use a new output directory:
python3 -B components/appearance/render.py \
--identity design/identity.json \
--output /tmp/niri-plus-appearance
The directory includes an identity and renderer receipt plus per-file hashes. Rendering reads the identity and writes only to the requested output directory. SDDM derivation additionally reads the separately pinned upstream theme tree; the stable helper below adds the preview ownership marker after the pure derivation returns its 23 core files.
Files and owners
| Download | Surface | Scope |
|---|---|---|
| Niri focus and background fragment | Niri | Pure-black workspace, charcoal overview surround and focus-ring colors. Merge layout settings into the existing block and the separate overview settings at top level; preserve sizing and navigation. |
| Rofi theme | Rofi launcher | Theme declarations and colors only. Existing actions, key bindings, and launcher configuration remain yours. |
| SwayNC stylesheet | Selected SwayNC fork | CSS only; imports the packaged base stylesheet. The fork still owns panel behavior and widget structure. |
| Gtklock stylesheet | Gtklock | Password-form appearance only. Authentication, lock-service lifetime, and power behavior stay with their current owners. |
| Qt 5 palette | qt5ct | QPalette colors only. Keep the selected Fusion style and portal dialog setting. |
| Qt 6 palette | qt6ct | QPalette colors only. Keep the selected Breeze style and portal dialog setting. |
| KDE color scheme | KDE / native Qt focus | Includes KDE Colors:* role groups. It is a color scheme file, not a replacement for kdeglobals. |
The SDDM greeter has a separate login/derivation recipe. Its theme and root installation remain separate from these user appearance files.
Dependencies and native variation
Use Noto Sans at 11pt for UI text, Source Han Sans CN as the CJK fallback, and
MesloLGS Nerd Font at 11pt for terminal text. Breeze Dark icons and
breeze_cursors are separate dependencies; the Niri fragment does not select
or install them. The website exports the Noto Sans license,
Meslo license, and Source Han license.
The SwayNC stylesheet targets the selected fork and its dashboard widgets. It does not recolor other GTK programs. Applications that follow Adwaita or Libadwaita keep their own supported colors and may look different from the specimens. Gtklock has its own explicit stylesheet because it owns a separate password surface.
The optional shared.swaync_sidebar identity object, available in the
0.4.3 maintenance release, enables the selected
translucent sidebar treatment. It accepts only opacity, border_color, and
card_border_color; without it, SwayNC rendering retains the legacy output.
The selected values set a 75% panel and card fill, a single left panel border,
and matching information and notification outlines. Text, focus accents,
critical edges, information dividers, and widget data keep their own roles.
For this reference account, GTK appearance uses the org.gnome.desktop.interface
GSettings values below. Leave GTK_THEME unset: GTK documents it as a theme
debug override that can take precedence over the selected system color
preference (GTK environment-variable reference).
Qt palettes set QPalette roles; they do not set Qt style, icon theme, portal
dialogs, or KDE's named focus colors. KDE also reads its owned [Colors:*]
groups from kdeglobals. Install NeutralSlate.colors under
~/.local/share/color-schemes/ and merge the generated
kdeglobals-managed.json values into the current kdeglobals. The renderer's
fonts.json supplies font names and sizes; do not duplicate generated palette
or font values by hand.
Apply the staged files on Arch
The main Arch walkthrough exports BUNDLE and APPEARANCE
after verifying the published component archive, identity, and public examples.
Use those rendered files for this install. Inspect existing target paths and
back up any files you will change; the merge below retains unrelated INI keys,
comments, and groups while replacing only its explicit appearance keys.
Copy the palette and stylesheet leaves to ordinary user paths:
install -Dm644 "$APPEARANCE/qt5-palette.conf" \
"$HOME/.config/qt5ct/colors/NeutralSlate.conf"
install -Dm644 "$APPEARANCE/qt6-palette.conf" \
"$HOME/.config/qt6ct/colors/NeutralSlate.conf"
install -Dm644 "$APPEARANCE/NeutralSlate.colors" \
"$HOME/.local/share/color-schemes/NeutralSlate.colors"
install -Dm644 "$APPEARANCE/neutral-slate.rasi" \
"$HOME/.config/rofi/neutral-slate.rasi"
install -Dm644 "$APPEARANCE/gtklock.css" \
"$HOME/.config/gtklock/style.css"
install -Dm644 "$APPEARANCE/kitty-neutral-slate.conf" \
"$HOME/.config/kitty/neutral-slate.conf"
Merge GTK3, Qt5/Qt6, KDE, and Gtklock settings with the ini_merge.py helper
from the verified bundle. The Qt font strings are generated from
fonts.json; kdeglobals-managed.json supplies KDE's scheme, font, icon, and
color groups:
python3 -B - "$BUNDLE" "$APPEARANCE" <<'PY'
import json
import os
import subprocess
import sys
from pathlib import Path
bundle, appearance = map(Path, sys.argv[1:])
sys.path.insert(0, str(bundle / "components/appearance"))
from ini_merge import merge_ini
home = Path.home()
fonts = json.loads((appearance / "fonts.json").read_text(encoding="utf-8"))
schema = "org.gnome.desktop.interface"
gsettings_keys = (
"gtk-theme", "color-scheme", "icon-theme", "cursor-theme",
"cursor-size", "font-name", "monospace-font-name",
)
state_path = home / ".local/state/niri-plus/appearance-gsettings-before.json"
state_path.parent.mkdir(parents=True, exist_ok=True)
if state_path.exists():
raise SystemExit(
f"preserve existing GSettings snapshot and choose a new path: {state_path}"
)
before = {}
for key in gsettings_keys:
value = subprocess.run(
["gsettings", "get", schema, key], check=True, text=True,
capture_output=True,
).stdout.removesuffix("\n")
explicit = subprocess.run(
["dconf", "read", f"/org/gnome/desktop/interface/{key}"],
check=True, text=True, capture_output=True,
).stdout.removesuffix("\n")
before[key] = {"gvariant": value, "dconf": explicit or None}
fd = os.open(state_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as stream:
json.dump({"schema": schema, "values": before}, stream, indent=2)
stream.write("\n")
def merge(relative, values):
path = home / relative
if path.is_symlink():
raise SystemExit(f"inspect symlink before merging: {path}")
original = path.read_text(encoding="utf-8") if path.exists() else ""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(merge_ini(original, values), encoding="utf-8")
merge(".config/gtk-3.0/settings.ini", {
"Settings": {
"gtk-theme-name": "adw-gtk3-dark",
"gtk-icon-theme-name": "breeze-dark",
"gtk-cursor-theme-name": "breeze_cursors",
"gtk-cursor-theme-size": "24",
"gtk-font-name": f"{fonts['ui_font']} {fonts['ui_pt']}",
},
})
for version, style in ((5, "Fusion"), (6, "Breeze")):
base = home / f".config/qt{version}ct"
qt_fonts = {
"general": f"\"{fonts['ui_font']},{fonts['ui_pt']},-1,5,50,0,0,0,0,0\"",
"fixed": f"\"{fonts['mono_font']},{fonts['terminal_pt']},-1,5,50,0,0,0,0,0\"",
}
merge(f".config/qt{version}ct/qt{version}ct.conf", {
"Appearance": {
"color_scheme_path": str(base / "colors/NeutralSlate.conf"),
"custom_palette": "true",
"icon_theme": "breeze-dark",
"style": style,
},
"Fonts": qt_fonts,
})
merge(".config/kdeglobals", json.loads(
(appearance / "kdeglobals-managed.json").read_text(encoding="utf-8")
))
merge(".config/gtklock/config.ini", {
"main": {"style": str(home / ".config/gtklock/style.css")},
})
gsettings_values = {
"gtk-theme": "adw-gtk3-dark",
"color-scheme": "prefer-dark",
"icon-theme": "breeze-dark",
"cursor-theme": "breeze_cursors",
"cursor-size": "24",
"font-name": f"{fonts['ui_font']} {fonts['ui_pt']}",
"monospace-font-name": f"{fonts['mono_font']} {fonts['terminal_pt']}",
}
for key, value in gsettings_values.items():
subprocess.run(["gsettings", "set", schema, key, value], check=True)
print(f"Saved prior GSettings and dconf values to {state_path}")
PY
The mode-0600 snapshot is created without overwriting an existing file. If it already exists, preserve it and choose a new matching path in the apply and restore blocks before retrying.
For SwayNC, use the rendered stylesheet and the selected dashboard config from
the notification recipe; that recipe installs the CSS at
~/.config/swaync/style.css after the pinned fork is installed.
Preview Rofi before selecting it in the existing ~/.config/rofi/config.rasi:
rofi -show drun -theme "$HOME/.config/rofi/neutral-slate.rasi"
In ~/.config/rofi/config.rasi, add this directive; preserve the file's other
launcher settings and actions:
@theme "~/.config/rofi/neutral-slate.rasi"
In the existing Kitty config, include neutral-slate.conf and set its font
from fonts.json (the installed Meslo face resolves as
MesloLGS Nerd Font Mono at 11pt). Merge these lines while retaining other
Kitty options. The terminal recipe selects /usr/bin/zsh --login
for new Kitty windows; that does not change the account's login shell:
font_family MesloLGS Nerd Font Mono
font_size 11.0
shell /usr/bin/zsh --login
include neutral-slate.conf
Keep the current shell, input, paste, glyph, and window behavior. The palette can be previewed without making it the default:
kitty --config "$HOME/.config/kitty/kitty.conf" \
--config "$HOME/.config/kitty/neutral-slate.conf"
Use the values in the staged $APPEARANCE/niri-neutral-slate.kdl fragment.
Merge its layout settings into the existing block and any separate overview
settings at top level. The selected identity uses workspace #000000, overview
surround #111213, active focus #a6ceff and inactive focus #81878c. An older
reference keeps the values in its own verified fragment. Preserve proportional
widths, gaps, focus-ring width, binds and other behavior.
Add the cursor selection only if the current config has no cursor block:
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 24
}
Validate the complete file before reloading Niri:
niri validate --config "$HOME/.config/niri/config.kdl"
Install the Qt platform setting for applications launched as systemd user services, then reload the user manager so subsequently started units receive it. GTK's theme and dark preference come from the GSettings values above:
install -Dm644 /dev/stdin \
"$HOME/.config/environment.d/90-niri-plus-appearance.conf" <<'EOF'
QT_QPA_PLATFORMTHEME=qt5ct
EOF
systemctl --user daemon-reload
systemctl --user unset-environment GTK_THEME
systemctl --user show-environment | grep '^QT_QPA_PLATFORMTHEME='
Already-running processes keep their current environment. Direct children
launched by Niri use Niri's environment block rather than the user-manager
environment. Merge the Qt platform value into that same existing Niri block;
leave GTK_THEME unset. The native Chrome and Kitty composition checks passed
without changing their input-method route:
QT_QPA_PLATFORMTHEME "qt5ct"
The tested Qt5ct 1.9 / Qt6ct 0.11 plugins both accept the qt5ct name. Native
Qt5 and Qt6 probes loaded their matching plugins with the same Neutral Slate
palette and Noto Sans 11. This single value also keeps Qt6ct's configuration
check clear; the combined qt5ct:qt6ct value triggered its warning.
Keep the input-method values in the input recipe, and do not add a global
GTK_IM_MODULE here.
Merge and preview
-
Download the fragment for the surface you want to review and retain a copy of its current configuration. Merge only the listed appearance layer; do not replace launcher actions, notification config, lock settings, or service definitions.
-
Preview the Rofi theme in a one-off launcher window before changing the normal theme selection:
rofi -show drun -theme "$PWD/examples/theme/neutral-slate.rasi" -
For SwayNC and Gtklock, merge the CSS into the stylesheet path already used by that component, then preview through its normal owner-managed reload or isolated session. Do not alter the independent lock unit or authentication configuration to preview colors.
-
For Qt 5/6, install the generated palette and merge the style/font settings above. Restart a test Qt application to inspect focus, selected, disabled, and dialog states; keep the selected platform styles and portal choices.
-
For KDE, install and merge the generated
.colorsmapping above, then confirm itsColors:*groups and the selected fonts inkdeglobals. For Niri, merge the fragment into the currentlayoutblock and validate the full config before using the normal reload path.
Apply one surface at a time and review its active, inactive, focused, selected, and disabled states. These files contain no input bindings, service startup, power policy, session ownership, or application action changes.
Restore GTK preferences
Use this recovery step only when backing out the reference appearance.
The JSON stores each exact gsettings get GVariant value and whether dconf had
an explicit value for that key. Restore only those seven keys; unset keys return
to their prior default state, while explicit values are written back exactly:
python3 - "$HOME/.local/state/niri-plus/appearance-gsettings-before.json" <<'PY'
import json
import subprocess
import sys
from pathlib import Path
state = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
schema = state["schema"]
for key, saved in state["values"].items():
if saved["dconf"] is None:
subprocess.run(["gsettings", "reset", schema, key], check=True)
else:
subprocess.run(
["dconf", "write", f"/org/gnome/desktop/interface/{key}", saved["dconf"]],
check=True,
)
PY