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

  1. 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.

  2. 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"
    
  3. 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.

  4. 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.

  5. For KDE, install and merge the generated .colors mapping above, then confirm its Colors:* groups and the selected fonts in kdeglobals. For Niri, merge the fragment into the current layout block 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
Setup overview →Read on GitHub ↗