Consolidate omarchy-shell colors into a single shell.toml surface

Five plugins each owned their own colors.toml watcher with subtly
different parsers, plus two ad-hoc per-theme override files
(notifications.json, image-picker-colors.json). This collapses all of
that into one source of truth: Commons/Color.qml watches colors.toml +
shell.toml and exposes Color.bar.*, Color.popups.*, Color.notifications.*,
Color.menu.*, Color.imagePicker.* for every surface to bind to.

shell.toml is generated by the existing template pipeline from
default/themed/shell.toml.tpl. Themes can ship their own shell.toml to
override individual keys; everything missing falls back to the
foundational palette via root.pick(). Settings panel is intentionally
not themable beyond the foundational tokens.

last-horizon and solitude ship a minimal shell.toml to preserve their
historical 'notification border matches Hyprland active border'
behavior, which previously came from parsing the theme's hyprland.conf
(now removed).
This commit is contained in:
Ryan Hughes
2026-05-16 00:30:27 -04:00
parent 227d32544f
commit f404a81c6a
14 changed files with 199 additions and 298 deletions
@@ -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 <sharp|round>` 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
@@ -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(/<img[^>]*>/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 {