diff --git a/CLAUDE.md b/CLAUDE.md
index 8785a3c..3779d32 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -27,6 +27,17 @@ Waybar-Toggle für wlsunset (Wayland-nativer Blaufilter):
- Signal SIGRTMIN+11 für sofortiges Waybar-Refresh
- Scripts: `moonarch-nightlight` (Toggle), `moonarch-waybar-nightlight` (Status-JSON)
+## Waybar Config Merger (moonarch-waybar)
+
+Waybar wird über `moonarch-waybar` gestartet (nicht direkt). Der Wrapper merged eine optionale User-Config (`~/.config/waybar/userconfig`) mit der System-Config (`/etc/xdg/waybar/config`):
+- `prepend`/`append`-Keys in der userconfig erweitern `modules-left`/`modules-center`/`modules-right` Arrays
+- Alle anderen Top-Level-Keys werden als Modul-Definitionen per Object-Merge eingefügt
+- Merge wird nur bei Änderungen ausgeführt (Timestamp-Vergleich)
+- Bei Fehler: `notify-send` + `logger`, Waybar startet mit System-Config
+- Generiert `~/.config/waybar/style.css` mit `@import` der System-Styles falls nicht vorhanden
+- Benötigt `jq` (in PKGBUILD als Dependency)
+- System-Config muss valides JSON sein (kein JSONC)
+
## Konventionen
- Paketlisten sind einfache Textdateien, ein Paket pro Zeile, Kommentare mit `#`
diff --git a/defaults/bin/moonarch-waybar b/defaults/bin/moonarch-waybar
new file mode 100755
index 0000000..3d3f446
--- /dev/null
+++ b/defaults/bin/moonarch-waybar
@@ -0,0 +1,59 @@
+#!/bin/bash
+# ABOUTME: Wrapper that merges system waybar config with per-machine userconfig.
+# ABOUTME: Handles array prepend/append that waybar's native include cannot do.
+
+SYSTEM_CONFIG="/etc/xdg/waybar/config"
+SYSTEM_STYLE="/etc/xdg/waybar/style.css"
+USER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/waybar"
+USERCONFIG="$USER_DIR/userconfig"
+OUTPUT="$USER_DIR/config"
+USER_STYLE="$USER_DIR/style.css"
+
+merge_config() {
+ mkdir -p "$USER_DIR"
+
+ if ! jq -s '
+ .[0] as $sys | .[1] as $user |
+ (($user.prepend // {}) | to_entries) as $prepends |
+ (($user.append // {}) | to_entries) as $appends |
+ $sys |
+ reduce $prepends[] as $p (.;
+ .[$p.key] = ($p.value + (.[$p.key] // []))
+ ) |
+ reduce $appends[] as $a (.;
+ .[$a.key] = ((.[$a.key] // []) + $a.value)
+ ) |
+ ($user | del(.prepend) | del(.append)) as $extras |
+ . * $extras
+ ' "$SYSTEM_CONFIG" "$USERCONFIG" > "${OUTPUT}.tmp" 2>&1; then
+ local err
+ err=$(cat "${OUTPUT}.tmp")
+ rm -f "${OUTPUT}.tmp"
+ logger -t moonarch-waybar "Config merge failed: $err"
+ notify-send -u critical "moonarch-waybar" "Config merge failed — using system config.\n$err"
+ return 1
+ fi
+
+ mv "${OUTPUT}.tmp" "$OUTPUT"
+}
+
+bootstrap_style() {
+ if [[ ! -f "$USER_STYLE" ]]; then
+ mkdir -p "$USER_DIR"
+ cat > "$USER_STYLE" << 'CSS'
+/* Generated by moonarch-waybar — add custom styles below */
+@import url("/etc/xdg/waybar/style.css");
+CSS
+ fi
+}
+
+if [[ -f "$USERCONFIG" ]]; then
+ if [[ ! -f "$OUTPUT" ]] ||
+ [[ "$USERCONFIG" -nt "$OUTPUT" ]] ||
+ [[ "$SYSTEM_CONFIG" -nt "$OUTPUT" ]]; then
+ merge_config
+ fi
+ bootstrap_style
+fi
+
+exec waybar "$@"
diff --git a/defaults/xdg/niri/config.kdl b/defaults/xdg/niri/config.kdl
index b7753c0..c8c9a87 100644
--- a/defaults/xdg/niri/config.kdl
+++ b/defaults/xdg/niri/config.kdl
@@ -79,7 +79,7 @@ layout {
// xwayland-satellite is managed automatically since niri 25.08
// kanshi is managed via systemd user service (kanshi.service)
-spawn-at-startup "waybar"
+spawn-at-startup "moonarch-waybar"
spawn-at-startup "swaync"
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
spawn-at-startup "nm-applet" "--indicator"
@@ -126,7 +126,7 @@ binds {
Super+C hotkey-overlay-title=null { spawn "walker" "-s" "clipboard"; }
- Alt+W { spawn-sh "killall waybar && waybar &"; }
+ Alt+W { spawn-sh "killall waybar && moonarch-waybar &"; }
Super+E { spawn-sh "xdg-open ~"; }
diff --git a/defaults/xdg/waybar/config b/defaults/xdg/waybar/config
index 1941f40..7022f6c 100644
--- a/defaults/xdg/waybar/config
+++ b/defaults/xdg/waybar/config
@@ -115,7 +115,6 @@
"on-scroll-down": "shift_down",
"on-click-middle": "alarm-clock-applet"
}
- // "on-click": "evolution"
},
"user": {
"format": "{user}",
@@ -191,7 +190,6 @@
"format": "{icon}",
"icon-size": 16,
"tooltip-format": "{title:.100}",
- // "sort-by-app-id": true,
"on-click": "activate",
"on-click-middle": "close",
"on-right-middle": "minimize-raise",
@@ -204,7 +202,7 @@
"firefox": "Firefox",
"VSCodium": "Code",
"codium": "Code",
- "Alacritty": "Terminal",
+ "Alacritty": "Terminal"
},
"squash-list": [
"firefox"
@@ -242,10 +240,8 @@
"escape": true
},
"cava": {
- // "cava_config": "$XDG_CONFIG_HOME/cava/cava.conf",
"framerate": 30,
"autosens": 1,
- //"sensitivity": 50,
"bars": 2,
"lower_cutoff_freq": 50,
"higher_cutoff_freq": 10000,
@@ -356,9 +352,8 @@
"signal": 9
},
"bluetooth": {
- // "controller": "controller1", // specify the alias of the controller if there are more than 1 on the system
"format": "",
- "format-disabled": "", // an empty format will hide the module
+ "format-disabled": "",
"format-connected": "{num_connections}",
"tooltip-format": "{controller_alias}\t{controller_address}",
"tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{device_enumerate}\t{device_battery_percentage}%",
@@ -410,13 +405,10 @@
"niri/workspaces": {
"format": "{icon}",
"format-icons": {
- // Named workspaces
- // (you need to configure them in niri)
"browser": "",
"discord": "",
"chat": "",
- // Icons by state
"active": "",
"default": ""
}
@@ -440,114 +432,56 @@
"device": "intel_backlight"
},
"cffi/niri-workspaces-enhanced": {
- // Make sure to set the path to the install location on your system
- // "module_path": "~/.config/waybar/niri-workspaces-enhanced.so",
"module_path": "/usr/lib/waybar/libwaybar_niri_workspaces_enhanced.so",
- // Format string for workspace labels. Available placeholders:
- // {index} - Workspace index number
- // {name} - Workspace name (might be empty)
- // {index-and-name} - Index followed by name if present (e.g., "1 Work")
- // {value} - Name if present, otherwise index
- // {separator} - ": " when icons are present, "" when empty
- // {window-icons} - Formatted icons for windows in workspace
"format": "{window-icons}",
- // Apply separate styles to icons depending on current state
"window-icon-format": {
"default": "{icon}",
"urgent": "{icon}",
- "focused": "{icon}",
+ "focused": "{icon}"
},
- // A mapping from window app_id to icon. Note that this module does
- // case-insensitive matching of app_ids, so capitalization doesn't matter.
"window-icons": {
"com.mitchellh.ghostty": "",
"darktable": "",
"foot": "",
"google-chrome": "",
"spotify": "",
- "steam": "",
+ "steam": ""
},
- // If no icon is found for a window, the default is used instead
- "window-icon-default": "*",
+ "window-icon-default": "*"
},
"height": 40,
"cffi/niri-windows": {
- // path where you placed the .so file
"module_path": "/usr/lib/waybar-niri-windows.so",
- // configure the module's behavior
"options": {
- // set the module mode
- // "graphical" (default): draw a minimap of windows in the current workspace
- // "text": draws symbols and a focus indicator for each column (mirrors v1 behavior)
"mode": "graphical",
- // ======= graphical mode options =======
- // when to show floating windows
- // - "always": always show floating window view, even if there are no floating windows
- // - "auto" (default): show floating window view if there are floating windows on the current workspace
- // - "never": never show floating windows
"show-floating": "auto",
- // pick where the floating windows be shown relative to tiled windows
- // - "left": show floating windows on the left
- // - "right" (default): show floating windows on the right
"floating-position": "right",
- // set minimum size of windows, in pixels (default: 1, minimum: 1)
- // if this value is too large to fit all windows (e.g. in a column with many windows),
- // it will be reduced
"minimum-size": 1,
- // set spacing between windows/columns, in pixels (default: 1, minimum: 0)
- // if this value is too large, it will be reduced
"spacing": 1,
- // set minimum size of windows, in pixels, to draw icons for (default: 0, minimum: 0)
- // if unset or 0, icons will only be drawn for tiled windows that are the only one in their column
- // if 1+, icons will be drawn for all windows where w >= icon-minimum-size and h >= icon-minimum-size
- // icons must be set in the "rules" section below for this to have any effect
"icon-minimum-size": 0,
- // account for borders when calculating window sizes; see note below (default: 0, minimum: 0)
- "column-borders": 0, // border on .column
- "floating-borders": 0, // border on .floating
- // trigger actions on tile click (see https://yalter.github.io/niri/niri_ipc/enum.Action.html for available actions)
- // only actions that take a single window ID are supported
- // set to an empty string to disable
- "on-tile-click": "FocusWindow", // (default: FocusWindow)
- "on-tile-middle-click": "CloseWindow", // (default: CloseWindow)
- "on-tile-right-click": "", // (default: none)
- // add CSS classes/icons to windows based on their App ID/Title (see `niri msg windows`)
- // Go regular expression syntax is supported for app-id and title (see https://pkg.go.dev/regexp/syntax)
- // rules are checked in the order they are defined - first match wins and checking stops
- // set "continue" to true to also check and apply subsequent rules even if this rule matches
- // if multiple rules with icons are applied, the first one will be used
- // *icons are not drawn for floating windows by default*; set "icon-minimum-size" to enable (see above)
+ "column-borders": 0,
+ "floating-borders": 0,
+ "on-tile-click": "FocusWindow",
+ "on-tile-middle-click": "CloseWindow",
+ "on-tile-right-click": "",
"rules": [
- // .alacritty will be added to all windows with the App ID "Alacritty"
- // will be drawn in windows that match
{ "app-id": "Alacritty", "class": "alacritty", "icon": "" },
- // .firefox will be added to all windows with the App ID "firefox"
- // subsequent rules are also checked and applied for firefox windows
{ "app-id": "firefox", "class": "firefox", "continue": true },
- // .youtube-music will be added to all windows that have "YouTube Music" at the end of their title
- // will be drawn in windows that match
{ "title": "YouTube Music$", "class": "youtube-music", "icon": "" }
],
- // ======= text mode options =======
- // customize the symbols used to draw the columns/windows
"symbols": {
"unfocused": "⋅",
"focused": "⊙",
"unfocused-floating": "∗",
"focused-floating": "⊛",
- // text to display when there are no windows on the current workspace
- // if this is an empty string (default), the module will be hidden when there are no windows
"empty": ""
}
},
"actions": {
- // use niri IPC action names to trigger them (see https://yalter.github.io/niri/niri_ipc/enum.Action.html for available actions)
- // any action that has no fields is supported
"on-scroll-up": "FocusColumnLeft",
"on-scroll-down": "FocusColumnRight"
- // in graphical mode, don't configure click actions here—they're handled by the module above
}
}
}
\ No newline at end of file