From 2363e76b4a7132fa6c0bf84800024e17e675b21e Mon Sep 17 00:00:00 2001 From: nevaforget Date: Thu, 9 Apr 2026 17:04:24 +0200 Subject: [PATCH] feat: add moonarch-waybar config merger wrapper Waybar's include directive cannot merge arrays, making per-machine module customization impossible without duplicating the entire config. moonarch-waybar merges an optional ~/.config/waybar/userconfig with the system config, supporting prepend/append on module arrays and object merge for module definitions. Generates user style.css with @import of system styles on first run. System waybar config converted from JSONC to valid JSON for jq compatibility. Niri startup and hotkey updated to use the wrapper. --- CLAUDE.md | 11 +++++ defaults/bin/moonarch-waybar | 59 +++++++++++++++++++++++++ defaults/xdg/niri/config.kdl | 4 +- defaults/xdg/waybar/config | 86 +++++------------------------------- 4 files changed, 82 insertions(+), 78 deletions(-) create mode 100755 defaults/bin/moonarch-waybar 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