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:
+5
-25
@@ -1,12 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Open a generic image selector menu
|
||||
# omarchy:args=[--selected <image>] [--colors-file <path>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] <image-dir>...
|
||||
# omarchy:args=[--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] <image-dir>...
|
||||
|
||||
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 <image>] [--colors-file <path>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] <image-dir>..."
|
||||
echo "Usage: omarchy-menu-images [--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] <image-dir>..."
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,13 @@ Two ways to drive it:
|
||||
|
||||
- Shell-level summon: `omarchy-shell-ipc shell summon omarchy.image-picker '<jsonPayload>'`.
|
||||
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 <imageDirs> <imageRowsB64> <selectedImage> <selectionFile> <doneFile> <colorsFile> <colorsRawB64> <showLabels> <filterable>`.
|
||||
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 <imageDirs> <imageRowsB64> <selectedImage> <selectionFile> <doneFile> <showLabels> <filterable>`.
|
||||
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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-17
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"primary": "{{ accent }}",
|
||||
"background": "{{ background }}",
|
||||
"backgroundText": "{{ foreground }}"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Omarchy shell colors. Defaults below derive from colors.toml. Any theme
|
||||
# can ship its own shell.toml at themes/<name>/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 }}"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user