diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index fe5aa14a..11862d2c 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -1,12 +1,11 @@ #!/bin/bash # omarchy:summary=Open a generic image selector menu -# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] ... +# omarchy:args=[--selected ] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] ... OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} selected_image="" -colors_file="" print_name=false show_labels=false filterable=false @@ -16,7 +15,7 @@ cache_only=false image_dirs=() usage() { - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] ..." + echo "Usage: omarchy-menu-images [--selected ] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] ..." } while [[ $# -gt 0 ]]; do @@ -30,15 +29,6 @@ while [[ $# -gt 0 ]]; do selected_image="$2" shift 2 ;; - --colors-file) - if (( $# < 2 )); then - usage >&2 - exit 1 - fi - - colors_file="$2" - shift 2 - ;; --print-name) print_name=true shift @@ -212,22 +202,14 @@ else fi fi -colors_file=${colors_file:-$HOME/.config/omarchy/current/theme/image-picker-colors.json} -colors_payload="" - -if [[ -f $colors_file ]]; then - colors_payload=$(<"$colors_file") -fi - if [[ $cache_only == true || $prepare_only == true ]]; then exit 0 fi -# Image rows and the raw colors blob can contain newlines and tabs, which -# don't survive a positional bash argv into `quickshell ipc call`. Base64 -# them; the ImagePicker plugin Qt.atob()s on the other side. +# Image rows can contain newlines and tabs, which don't survive a positional +# bash argv into `quickshell ipc call`. Base64-encode for transit; the +# ImagePicker plugin Qt.atob()s on the other side. rows_b64=$(printf '%s' "$rows" | base64 -w 0) -colors_b64=$(printf '%s' "$colors_payload" | base64 -w 0) if ! omarchy-shell-ipc image-selector open \ "" \ @@ -235,8 +217,6 @@ if ! omarchy-shell-ipc image-selector open \ "$selected_list_image" \ "$selection_file" \ "$done_file" \ - "" \ - "$colors_b64" \ "$show_labels" \ "$filterable" >/dev/null; then echo "Image selector failed to accept request" >&2 diff --git a/default/quickshell/omarchy-shell/Commons/Color.qml b/default/quickshell/omarchy-shell/Commons/Color.qml index fc3d35ec..a812eef6 100644 --- a/default/quickshell/omarchy-shell/Commons/Color.qml +++ b/default/quickshell/omarchy-shell/Commons/Color.qml @@ -3,27 +3,63 @@ import QtQuick import Quickshell import Quickshell.Io -// Noctalia compat shim. Plugins import `qs.Commons` and reach for these -// palette tokens / helpers. Values stream from the Omarchy theme file; the -// shell wires this singleton up at startup via setHostBar()/setHostShell(). -// -// We don't try to replicate Noctalia's full Material You resolver — for the -// fields plugins actually read, a flat token table is enough. +// Single source of truth for shell color surfaces. Top-level tokens +// (foreground/background/accent/urgent) come from the theme's colors.toml. +// Per-surface roles (Color.bar.*, Color.popups.*, Color.notifications.*, +// Color.menu.*, Color.imagePicker.*) come from shell.toml, which is generated +// per theme from default/themed/shell.toml.tpl (or shipped directly by a +// theme to override). Surfaces that don't appear in shell.toml fall back to +// the foundational palette, so themes can ship partial overrides. QtObject { id: root - // Live updated from theme/colors.toml via the FileView below. + // Foundational palette. Live updated from theme/colors.toml. property color foreground: "#cacccc" property color background: "#101315" property color accent: "#cacccc" property color urgent: "#a55555" - // The theme's Hyprland active border color (`$activeBorderColor` in the - // theme's hyprland.conf). Most themes set this to the accent; some themes - // (e.g. aether, oodle) override it to a neutral. Streamed from the theme - // file alongside colors.toml so notification surfaces stay in sync. - property color border: accent - // Noctalia palette tokens. We map them onto our theme colors. + // Flat dictionary of "section.key" -> "#rrggbb" parsed from shell.toml. + // Reassigning this whole property is what makes surface bindings below + // re-evaluate when the theme swaps; mutating it in place would not. + property var shellValues: ({}) + + function pick(key, fallback) { + var v = shellValues[key] + return (typeof v === "string" && v.length > 0) ? v : fallback + } + + // Surface roles. Each property reads its shell.toml override if set, + // otherwise falls back to a foundational palette token. + readonly property QtObject bar: QtObject { + property color background: root.pick("bar.background", root.background) + property color text: root.pick("bar.text", root.foreground) + property color active: root.pick("bar.active", root.urgent) + } + readonly property QtObject popups: QtObject { + property color background: root.pick("popups.background", root.background) + property color border: root.pick("popups.border", root.foreground) + } + readonly property QtObject notifications: QtObject { + property color background: root.pick("notifications.background", root.background) + property color text: root.pick("notifications.text", root.foreground) + property color border: root.pick("notifications.border", root.accent) + property color countdown: root.pick("notifications.countdown", root.accent) + } + readonly property QtObject menu: QtObject { + property color background: root.pick("menu.background", root.background) + property color text: root.pick("menu.text", root.foreground) + property color selected: root.pick("menu.selected", root.accent) + } + readonly property QtObject imagePicker: QtObject { + property color background: root.pick("image-picker.background", root.background) + property color text: root.pick("image-picker.text", root.foreground) + property color selectedBorder: root.pick("image-picker.selected-border", root.accent) + property color unselectedBorder: root.pick("image-picker.unselected-border", root.foreground) + } + + // Noctalia palette tokens used by the compat widgets. Mapped onto the + // foundational palette; not exposed in shell.toml. readonly property color mPrimary: accent readonly property color mSecondary: Qt.darker(accent, 1.2) readonly property color mTertiary: Qt.lighter(accent, 1.3) @@ -89,12 +125,10 @@ QtObject { case "urgent": case "red": return urgent } - // Anything else, treat as a literal CSS color string (the QML color type - // does this conversion implicitly when assigned). return Qt.color(k) } - function loadTheme(raw) { + function loadColors(raw) { var lines = String(raw || "").split("\n") var foundAccent = false var color4Value = "" @@ -114,39 +148,52 @@ QtObject { if (!foundAccent && color4Value.length > 0) accent = color4Value } - // Parse `$activeBorderColor = rgb(XXXXXX)` from the theme's hyprland.conf. - function loadHyprlandTheme(raw) { - var match = String(raw || "").match(/\$activeBorderColor\s*=\s*rgba?\(([0-9A-Fa-f]{6,8})\)/) - if (!match) { border = accent; return } - var hex = match[1] - // Hyprland rgba() is RRGGBBAA; strip the AA suffix and use RGB. - if (hex.length === 8) hex = hex.substring(0, 6) - border = "#" + hex + // Walk shell.toml line-by-line. We only need string values for color keys, + // and the file is small, so no proper TOML parser. Accepts double- or + // single-quoted values and tolerates trailing inline comments. + function loadShell(raw) { + var parsed = {} + var text = String(raw || "") + if (text) { + var lines = text.split("\n") + var section = "" + for (var i = 0; i < lines.length; i++) { + var line = lines[i].replace(/^\s+|\s+$/g, "") + if (!line || line.charAt(0) === "#") continue + var sectionMatch = line.match(/^\[([A-Za-z0-9_-]+)\]\s*(#.*)?$/) + if (sectionMatch) { section = sectionMatch[1]; continue } + var kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*["']([^"']+)["']\s*(#.*)?$/) + if (!kv || !section) continue + parsed[section + "." + kv[1]] = kv[2] + } + } + shellValues = parsed } // `omarchy-theme-set` recreates the theme/ directory via rm+mv, which kills // the inotify watch on colors.toml. Use theme.name (overwritten in place) as // a tripwire that forces a fresh reload after each swap. - property FileView themeFile: FileView { - id: themeColorsFile + property FileView colorsFile: FileView { + id: colorsFile path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/colors.toml" watchChanges: true printErrors: false - onLoaded: root.loadTheme(text()) + onLoaded: root.loadColors(text()) + onFileChanged: reload() + } + property FileView shellFile: FileView { + id: shellFile + path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/shell.toml" + watchChanges: true + printErrors: false + onLoaded: root.loadShell(text()) + onLoadFailed: root.loadShell("") onFileChanged: reload() } property FileView themeNameFile: FileView { path: Quickshell.env("HOME") + "/.config/omarchy/current/theme.name" watchChanges: true printErrors: false - onFileChanged: { themeColorsFile.reload(); themeHyprlandFile.reload() } - } - property FileView themeHyprlandFile: FileView { - id: themeHyprlandFile - path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/hyprland.conf" - watchChanges: true - printErrors: false - onLoaded: root.loadHyprlandTheme(text()) - onFileChanged: reload() + onFileChanged: { colorsFile.reload(); shellFile.reload() } } } diff --git a/default/quickshell/omarchy-shell/plugins/README.md b/default/quickshell/omarchy-shell/plugins/README.md index c4d235be..0184f91e 100644 --- a/default/quickshell/omarchy-shell/plugins/README.md +++ b/default/quickshell/omarchy-shell/plugins/README.md @@ -47,12 +47,13 @@ Two ways to drive it: - Shell-level summon: `omarchy-shell-ipc shell summon omarchy.image-picker ''`. The payload can carry `imageDirs`, `imageRows`, `selectedImage`, - `selectionFile`, `doneFile`, `colorsFile`, `colorsRaw`, `showLabels`, - `filterable`. Best for in-shell callers that already speak JSON. -- Direct IPC target: `omarchy-shell-ipc image-selector open `. - Positional args; `imageRowsB64` and `colorsRawB64` are base64-encoded so - embedded newlines / tabs survive the bash argv handoff. This is what - `omarchy-menu-images` uses. + `selectionFile`, `doneFile`, `showLabels`, `filterable`. Best for + in-shell callers that already speak JSON. +- Direct IPC target: `omarchy-shell-ipc image-selector open `. + Positional args; `imageRowsB64` is base64-encoded so embedded newlines / + tabs survive the bash argv handoff. This is what `omarchy-menu-images` + uses. Colors come from the central shell theme singleton; there is no + per-call override surface. The selection round-trip remains file-based: callers create a `selection_file` and `done_file` (both `mktemp`), pass the paths, and diff --git a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml index 867fd112..23d6c4ff 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml @@ -9,6 +9,7 @@ import Quickshell.Wayland import Quickshell.Widgets import QtQuick import QtQuick.Layouts +import qs.Commons import "common" as BarCommon Item { @@ -49,9 +50,11 @@ Item { // "monospace" resolves through fontconfig at paint time, so changing the // system font (via `omarchy-font-set`) updates the bar without a reload. property string fontFamily: "monospace" - property color foreground: "#cacccc" - property color background: "#101315" - property color urgent: "#a55555" + // Bound to the central Color singleton so the bar tracks shell.toml's + // [bar] section. Property names kept for the rest of this file's bindings. + property color foreground: Color.bar.text + property color background: Color.bar.background + property color urgent: Color.bar.active property string weatherText: "" property string weatherClass: "" property bool updateAvailable: false @@ -438,18 +441,6 @@ Item { } } - function loadTheme(raw) { - var lines = String(raw || "").split("\n") - for (var i = 0; i < lines.length; i++) { - var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/) - if (!match) continue - - if (match[1] === "foreground") foreground = match[2] - else if (match[1] === "background") background = match[2] - else if (match[1] === "red") urgent = match[2] - } - } - function updateWeather(raw) { var data = parseModuleJson(raw) weatherText = data.text || "" @@ -723,26 +714,6 @@ Item { onTriggered: root.tooltipShown = true } - // The host owns shell.json loading and injects `barConfig`. Bar still keeps - // its own theme FileView since theme colors are independent of shell.json. - // `omarchy-theme-set` recreates the entire theme/ directory via rm+mv, which - // invalidates the inotify watch on colors.toml. Use theme.name (overwritten - // in place) to force a fresh reload after each swap. - FileView { - id: themeColorsFile - path: root.home + "/.config/omarchy/current/theme/colors.toml" - watchChanges: true - printErrors: false - onLoaded: root.loadTheme(text()) - onFileChanged: reload() - } - FileView { - path: root.home + "/.config/omarchy/current/theme.name" - watchChanges: true - printErrors: false - onFileChanged: themeColorsFile.reload() - } - // Presence of the `bar-off` flag = bar hidden. Watching the parent toggles // directory because FileView can't observe a file that doesn't exist yet, // and the flag is created/removed by `omarchy-toggle-bar`. diff --git a/default/quickshell/omarchy-shell/plugins/bar/common/PopupCard.qml b/default/quickshell/omarchy-shell/plugins/bar/common/PopupCard.qml index de45bae4..1f6cd155 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/common/PopupCard.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/common/PopupCard.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Hyprland +import qs.Commons PopupWindow { id: root @@ -89,8 +90,8 @@ PopupWindow { Rectangle { id: card anchors.fill: parent - color: root.bar ? root.bar.background : "#101315" - border.color: root.bar ? root.bar.foreground : "#cacccc" + color: Color.popups.background + border.color: Color.popups.border border.width: 1 radius: 0 opacity: root.open ? 1.0 : 0 diff --git a/default/quickshell/omarchy-shell/plugins/image-picker/ImagePicker.qml b/default/quickshell/omarchy-shell/plugins/image-picker/ImagePicker.qml index 25f5b2cd..96054960 100644 --- a/default/quickshell/omarchy-shell/plugins/image-picker/ImagePicker.qml +++ b/default/quickshell/omarchy-shell/plugins/image-picker/ImagePicker.qml @@ -4,6 +4,7 @@ import Quickshell.Wayland import QtQuick import QtQuick.Effects import QtQuick.Shapes +import qs.Commons Item { id: root @@ -22,7 +23,6 @@ Item { property string imageRows: "" property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE") property string selectedImage: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTED") - property string colorsFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_COLORS_FILE") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/image-picker-colors.json") property int selectedIndex: 0 property bool imagesLoaded: false property bool opened: false @@ -34,9 +34,11 @@ Item { property string doneFile: "" property string filterText: "" property var doneFilesToRelease: [] - property color accent: "#798186" - property color background: "#101315" - property color foreground: "#cacccc" + // Bound to the central [image-picker] section in shell.toml via Color.qml. + property color background: Color.imagePicker.background + property color foreground: Color.imagePicker.text + property color selectedBorder: Color.imagePicker.selectedBorder + property color unselectedBorder: Color.imagePicker.unselectedBorder property int expandedWidth: 768 property int expandedHeight: 475 property int sliceWidth: 108 @@ -250,7 +252,7 @@ Item { carousel.forceActiveFocus() } - function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw, nextShowLabels, nextFilterable) { + function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextShowLabels, nextFilterable) { if (requestActive && doneFile && doneFile !== nextDoneFile) finishDoneFile(doneFile) @@ -265,9 +267,6 @@ Item { showLabels = nextShowLabels === true || nextShowLabels === "true" filterable = nextFilterable === true || nextFilterable === "true" filterText = "" - colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/image-picker-colors.json") - if (nextColorsRaw) - loadColors(nextColorsRaw) imageArray = [] selectedIndex = 0 imagesLoaded = false @@ -320,11 +319,9 @@ Item { var sel = String(args.selectedImage || selectedImage) var selFile = String(args.selectionFile || "") var doneF = String(args.doneFile || "") - var colors = String(args.colorsFile || colorsFile) - var colorsRaw = String(args.colorsRaw || "") var labels = args.showLabels === true || args.showLabels === "true" var filter = args.filterable === true || args.filterable === "true" - openSelector(dirs, rows, sel, selFile, doneF, colors, colorsRaw, labels, filter) + openSelector(dirs, rows, sel, selFile, doneF, labels, filter) } function close() { @@ -332,9 +329,9 @@ Item { } // IPC surface. All arguments are strings (Quickshell IPC marshalling). - // imageRows and colorsRaw can contain newlines/tabs, so the CLI caller - // base64-encodes them; everything else passes through verbatim. The two - // boolean-like fields use the literal strings "true" or "false". + // imageRows can contain newlines/tabs, so the CLI caller base64-encodes + // it; everything else passes through verbatim. The two boolean-like + // fields use the literal strings "true" or "false". IpcHandler { target: "image-selector" @@ -343,14 +340,11 @@ Item { selectedImage: string, selectionFile: string, doneFile: string, - colorsFile: string, - colorsRawB64: string, showLabels: string, filterable: string): string { var rows = root.decodeBase64(imageRowsB64) - var colorsRaw = root.decodeBase64(colorsRawB64) root.openSelector(imageDirs, rows, selectedImage, selectionFile, doneFile, - colorsFile, colorsRaw, showLabels, filterable) + showLabels, filterable) return "ok" } @@ -363,25 +357,6 @@ Item { } } - // Tolerate the file being absent (e.g. theme templates not yet re-rendered - // after the rename) without a startup warning. - FileView { - path: root.colorsFile - watchChanges: true - printErrors: false - onLoaded: root.loadColors(text()) - onFileChanged: reload() - } - - function loadColors(raw) { - try { - var colors = JSON.parse(raw || "{}") - root.accent = colors.primary || root.accent - root.background = colors.background || root.background - root.foreground = colors.backgroundText || root.foreground - } catch (e) {} - } - Process { id: applyProc onExited: { @@ -557,7 +532,7 @@ Item { preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: "transparent" - strokeColor: item.selected ? root.accent : root.withAlpha(root.foreground, 0.28) + strokeColor: item.selected ? root.selectedBorder : root.withAlpha(root.unselectedBorder, 0.28) strokeWidth: item.selected ? 3 : 1 startX: item.topLeft; startY: 0 PathLine { x: item.topRight; y: 0 } diff --git a/default/quickshell/omarchy-shell/plugins/menu/Menu.qml b/default/quickshell/omarchy-shell/plugins/menu/Menu.qml index 2d374795..0fc4be28 100644 --- a/default/quickshell/omarchy-shell/plugins/menu/Menu.qml +++ b/default/quickshell/omarchy-shell/plugins/menu/Menu.qml @@ -2,6 +2,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick +import qs.Commons Item { id: root @@ -15,22 +16,13 @@ Item { // Plugin lifecycle hooks. The host calls open(payloadJson) after // `omarchy-shell-ipc shell summon omarchy.menu ...` and close() when hidden. property string pendingInitialMenu: "root" - property string pendingColorsRaw: "" - - function decodeBase64(value) { - var s = String(value || "") - if (!s) return "" - try { return Qt.atob(s) } catch (e) { return s } - } function open(payloadJson) { var payload = ({}) try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) } root.pendingInitialMenu = payload.initialMenu || payload.menu || "root" - root.pendingColorsRaw = payload.colorsRawBase64 ? root.decodeBase64(payload.colorsRawBase64) : (payload.colorsRaw || "") if (payload.fontFamily) root.fontFamily = payload.fontFamily - if (root.pendingColorsRaw) root.loadColors(root.pendingColorsRaw) root.openExistingMenu(root.pendingInitialMenu) } @@ -40,7 +32,6 @@ Item { } property string fontFamily: Quickshell.env("OMARCHY_MENU_FONT") || "monospace" - property string colorsFile: Quickshell.env("OMARCHY_MENU_COLORS_FILE") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/colors.toml") property string styleFile: Quickshell.env("OMARCHY_MENU_STYLE_FILE") || (Quickshell.env("HOME") + "/.local/state/omarchy/toggles/quickshell-menu.json") // JSONC menu definitions. The shell parses both at startup and merges // the user file on top of the defaults, so the keybind → IPC → visible @@ -61,9 +52,10 @@ Item { property var navStack: [] property var providersLoaded: ({}) property var providerQueue: [] - property color accent: "#89b4fa" - property color background: "#101315" - property color foreground: "#cacccc" + // Bound to the central [menu] section in shell.toml via Color.qml. + property color accent: Color.menu.selected + property color background: Color.menu.background + property color foreground: Color.menu.text property color border: foreground property int cornerRadius: 0 property int contentMargin: 18 @@ -624,18 +616,6 @@ Item { Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } - function loadColors(raw) { - var lines = String(raw || "").split("\n") - for (var i = 0; i < lines.length; i++) { - var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/) - if (!match) continue - if (match[1] === "foreground") foreground = match[2] - else if (match[1] === "background") background = match[2] - else if (match[1] === "color4" || match[1] === "accent") accent = match[2] - } - border = foreground - } - function loadStyle(raw) { try { var style = JSON.parse(raw || "{}") @@ -811,13 +791,6 @@ Item { } } - FileView { - path: root.colorsFile - watchChanges: true - onLoaded: root.loadColors(text()) - onFileChanged: { reload(); root.loadColors(text()) } - } - FileView { path: root.styleFile watchChanges: true diff --git a/default/quickshell/omarchy-shell/plugins/notifications/Service.qml b/default/quickshell/omarchy-shell/plugins/notifications/Service.qml index 90887dfd..70a005dd 100644 --- a/default/quickshell/omarchy-shell/plugins/notifications/Service.qml +++ b/default/quickshell/omarchy-shell/plugins/notifications/Service.qml @@ -33,17 +33,6 @@ Item { readonly property string cacheDir: home + "/.cache/omarchy/" readonly property string imageCacheDir: cacheDir + "notification-images/" readonly property string styleStatePath: home + "/.local/state/omarchy/toggles/quickshell-menu.json" - // Optional per-theme override file. Themes that want notification colors - // to diverge from the rest of the palette can ship this file. Schema: - // { - // "borderColor": "#hex", - // "backgroundColor": "#hex", - // "textColor": "#hex", - // "countdownColor": "#hex" // progress bar at bottom of popup - // } - // All keys optional; unset keys fall back to the live Color.* tokens. - readonly property string themeOverridePath: home + "/.config/omarchy/current/theme/notifications.json" - readonly property string themeNamePath: home + "/.config/omarchy/current/theme.name" // Corner radius is shared with omarchy-shell menu and settings panel — // `omarchy style corners ` writes this file once and every @@ -78,57 +67,6 @@ Item { onFileChanged: reload() } - // ----------------------------------------------- per-theme color overrides - - property string overrideBorder: "" - property string overrideBackground: "" - property string overrideText: "" - property string overrideCountdown: "" - - // Match the compact mako-style layout, but keep colors live-bound to the - // current Omarchy theme unless a theme explicitly ships notifications.json. - readonly property color effectiveBorder: overrideBorder.length > 0 ? overrideBorder : Color.accent - readonly property color effectiveBackground: overrideBackground.length > 0 ? overrideBackground : Color.background - readonly property color effectiveText: overrideText.length > 0 ? overrideText : Color.foreground - readonly property color effectiveCountdown: overrideCountdown.length > 0 ? overrideCountdown : Color.accent - - function loadThemeOverride(raw) { - try { - var parsed = JSON.parse(raw || "{}") - overrideBorder = typeof parsed.borderColor === "string" ? parsed.borderColor : "" - overrideBackground = typeof parsed.backgroundColor === "string" ? parsed.backgroundColor : "" - overrideText = typeof parsed.textColor === "string" ? parsed.textColor : "" - overrideCountdown = typeof parsed.countdownColor === "string" ? parsed.countdownColor : "" - } catch (e) { - overrideBorder = "" - overrideBackground = "" - overrideText = "" - overrideCountdown = "" - } - } - - FileView { - id: themeOverrideFile - path: service.themeOverridePath - watchChanges: true - printErrors: false - onLoaded: service.loadThemeOverride(text()) - onLoadFailed: service.loadThemeOverride("") - onFileChanged: reload() - } - - // Theme switching recreates ~/.config/omarchy/current/theme, which can leave - // the notifications.json watcher attached to the old symlink target. Watch - // theme.name (overwritten in place) as the stable tripwire and force a fresh - // override reload; if the new theme has no override, onLoadFailed clears the - // old colors instead of leaving stale theme-specific notification styling. - FileView { - path: service.themeNamePath - watchChanges: true - printErrors: false - onFileChanged: themeOverrideFile.reload() - } - // Fired by IPC (`omarchy-shell-ipc notifications showHistory`) so the // bar widget can drop its PopupCard from the same anchor a click would. signal historyOpenRequested() @@ -948,10 +886,6 @@ Item { urgency: cardSlot.urgency timestamp: cardSlot.timestamp cornerRadius: service.cornerRadius - borderColorOverride: service.effectiveBorder - backgroundColorOverride: service.effectiveBackground - textColorOverride: service.effectiveText - countdownColorOverride: service.effectiveCountdown fontFamily: service.shell && service.shell.bar ? service.shell.bar.fontFamily : "" glyph: cardSlot.glyph progress: cardSlot.progress diff --git a/default/quickshell/omarchy-shell/plugins/notifications/components/NotificationCard.qml b/default/quickshell/omarchy-shell/plugins/notifications/components/NotificationCard.qml index c004f9b3..88f1dd33 100644 --- a/default/quickshell/omarchy-shell/plugins/notifications/components/NotificationCard.qml +++ b/default/quickshell/omarchy-shell/plugins/notifications/components/NotificationCard.qml @@ -27,13 +27,6 @@ Rectangle { property real progress: 1.0 property bool showProgress: false - // Container can override the theme defaults per-card. Defaults bind to - // the live Color.* tokens, so when the container leaves them alone the - // card still tracks the theme. - property color borderColorOverride: Color.border - property color backgroundColorOverride: Color.background - property color textColorOverride: Color.foreground - property color countdownColorOverride: Color.accent // System font from shell.json bar.fontFamily, injected by the container. property string fontFamily: "" @@ -77,10 +70,10 @@ Rectangle { readonly property bool hasGlyph: glyph.length > 0 readonly property bool hasSmallIcon: !mediaMode && (smallIconSource.length > 0 || hasGlyph) - readonly property color dimColor: Qt.darker(textColorOverride, 1.4) - readonly property color bodyColor: Qt.darker(textColorOverride, 1.15) - readonly property color hoverColor: Qt.rgba(textColorOverride.r, textColorOverride.g, textColorOverride.b, 0.14) - readonly property color accentColor: urgency === 2 ? Color.urgent : (urgency === 0 ? dimColor : countdownColorOverride) + readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4) + readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15) + readonly property color hoverColor: Qt.rgba(Color.notifications.text.r, Color.notifications.text.g, Color.notifications.text.b, 0.14) + readonly property color accentColor: urgency === 2 ? Color.urgent : (urgency === 0 ? dimColor : Color.notifications.countdown) function sanitizeBody(s) { return String(s).replace(/]*>/gi, "") @@ -92,8 +85,8 @@ Rectangle { // for symmetry except when the progress bar replaces it. implicitHeight: mainColumn.implicitHeight + border.width * 2 radius: cornerRadius - color: backgroundColorOverride - border.color: urgency === 2 ? Color.urgent : borderColorOverride + color: Color.notifications.background + border.color: urgency === 2 ? Color.urgent : Color.notifications.border border.width: 2 clip: true @@ -145,7 +138,7 @@ Rectangle { anchors.right: parent.right anchors.bottom: parent.bottom height: root.border.width - color: root.urgency === 2 ? Color.urgent : root.borderColorOverride + color: root.urgency === 2 ? Color.urgent : Color.notifications.border } MouseArea { @@ -194,7 +187,7 @@ Rectangle { anchors.centerIn: parent visible: root.hasGlyph && smallIconImage.status !== Image.Ready text: root.glyph - color: root.textColorOverride + color: Color.notifications.text font.family: root.fontFamily font.pixelSize: 18 } @@ -210,7 +203,7 @@ Rectangle { visible: root.summary.length > 0 text: root.summary font.family: root.fontFamily - color: root.textColorOverride + color: Color.notifications.text font.pixelSize: 13 font.bold: true wrapMode: Text.WordWrap @@ -243,7 +236,7 @@ Rectangle { anchors.right: parent.right anchors.bottom: parent.bottom height: 3 - color: root.borderColorOverride + color: Color.notifications.border visible: false Rectangle { diff --git a/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml b/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml index bf684db4..bce83d22 100644 --- a/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml +++ b/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Io +import qs.Commons import "../../ui/settings" as SettingsUi import "./components" as Cmp @@ -59,10 +60,12 @@ Item { readonly property string styleStatePath: home + "/.local/state/omarchy/toggles/quickshell-menu.json" // ---------------- theme -------------------------------------------------- - property color foreground: "#cacccc" - property color background: "#101315" - property color accent: "#cacccc" - property color urgent: "#a55555" + // Settings panel deliberately isn't a themable surface in shell.toml — + // it tracks the foundational palette so every theme renders consistently. + property color foreground: Color.foreground + property color background: Color.background + property color accent: Color.accent + property color urgent: Color.urgent property string fontFamily: "monospace" // Source-of-truth for the shell-wide corner radius. Mirrors what the menu @@ -353,18 +356,6 @@ Item { mutateSection(section, function(a) { a[index] = cloneJson(newEntry) }) } - function loadTheme(raw) { - var lines = String(raw || "").split("\n") - for (var i = 0; i < lines.length; i++) { - var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/) - if (!match) continue - if (match[1] === "foreground") foreground = match[2] - else if (match[1] === "background") background = match[2] - else if (match[1] === "color4" || match[1] === "accent") accent = match[2] - else if (match[1] === "red") urgent = match[2] - } - } - function loadStyleState(raw) { try { var s = JSON.parse(raw || "{}") @@ -559,26 +550,6 @@ Item { onFileChanged: reload() } - // `omarchy-theme-set` does `rm -rf current/theme && mv next-theme current/theme`. - // The atomic swap invalidates the inotify watch on colors.toml (the file's - // inode is gone), so onFileChanged never fires for theme switches. Use - // theme.name — a stable file overwritten in place — as the tripwire and - // force-reload colors.toml from its new path each time it changes. - FileView { - id: themeColorsFile - path: root.home + "/.config/omarchy/current/theme/colors.toml" - watchChanges: true - printErrors: false - onLoaded: root.loadTheme(text()) - onFileChanged: reload() - } - FileView { - path: root.home + "/.config/omarchy/current/theme.name" - watchChanges: true - printErrors: false - onFileChanged: themeColorsFile.reload() - } - FileView { path: root.styleStatePath watchChanges: true diff --git a/default/themed/image-picker-colors.json.tpl b/default/themed/image-picker-colors.json.tpl deleted file mode 100644 index 96a92436..00000000 --- a/default/themed/image-picker-colors.json.tpl +++ /dev/null @@ -1,5 +0,0 @@ -{ - "primary": "{{ accent }}", - "background": "{{ background }}", - "backgroundText": "{{ foreground }}" -} diff --git a/default/themed/shell.toml.tpl b/default/themed/shell.toml.tpl new file mode 100644 index 00000000..63f23143 --- /dev/null +++ b/default/themed/shell.toml.tpl @@ -0,0 +1,50 @@ +# Omarchy shell colors. Defaults below derive from colors.toml. Any theme +# can ship its own shell.toml at themes//shell.toml to override +# individual keys — anything left out falls back to the values here. + +[bar] +# The bar strip itself +background = "{{ background }}" +# Clock, weather, workspace numbers, indicator glyphs +text = "{{ foreground }}" +# Module in an "active" state: screen recording, voxtype listening, +# weather alert, update available. Same color is used everywhere a +# module wants to call attention to itself. +active = "{{ color1 }}" + +[popups] +# Body fill for every flyout opened from the bar: wifi, bluetooth, audio, +# calendar, weather, control-center, notification-center, media. Body text +# inside the flyouts is not separately themable — it follows [bar].text. +background = "{{ background }}" +# 1px outline around the flyout +border = "{{ foreground }}" + +[notifications] +# Toast card body +background = "{{ background }}" +# Title and message text +text = "{{ foreground }}" +# Card outline. Most themes match this to their Hyprland active-window +# border so notifications visually belong to the focused window. +border = "{{ accent }}" +# Bottom countdown bar that drains while the toast is on screen +countdown = "{{ accent }}" + +[menu] +# Omarchy menu surface (the launcher-style picker invoked by the menu key) +background = "{{ background }}" +# Unhighlighted menu items +text = "{{ foreground }}" +# Currently hovered / keyboard-selected item +selected = "{{ accent }}" + +[image-picker] +# Backdrop scrim behind the picker (the picker draws this at ~70% alpha) +background = "{{ background }}" +# Tile labels (image filename / theme name) +text = "{{ foreground }}" +# 3px stroke around the currently selected tile +selected-border = "{{ accent }}" +# 1px stroke around every other tile (picker applies ~28% alpha) +unselected-border = "{{ foreground }}" diff --git a/themes/last-horizon/shell.toml b/themes/last-horizon/shell.toml new file mode 100644 index 00000000..d0bea2a1 --- /dev/null +++ b/themes/last-horizon/shell.toml @@ -0,0 +1,5 @@ +# last-horizon preserves the historical behavior of matching notification +# borders to the theme's Hyprland active-window border ($activeBorderColor +# in hyprland.conf), rather than the accent. +[notifications] +border = "#8a8588" diff --git a/themes/solitude/shell.toml b/themes/solitude/shell.toml new file mode 100644 index 00000000..5f2e138b --- /dev/null +++ b/themes/solitude/shell.toml @@ -0,0 +1,5 @@ +# solitude preserves the historical behavior of matching notification +# borders to the theme's Hyprland active-window border ($activeBorderColor +# in hyprland.conf), rather than the accent. +[notifications] +border = "#798186"