From 79470dde9229a8ec835aaba4e08832d35fc34765 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 3 Jun 2026 22:14:48 +0200 Subject: [PATCH] Refine shell panel and lock theming --- bin/omarchy-theme-set-templates | 79 ++++++++++++++++++++++++ docs/theming.md | 13 ++++ shell/Ui/KeyboardPanel.qml | 17 +++-- shell/plugins/bar/Bar.qml | 31 +++++++--- shell/plugins/bar/widgets/Indicators.qml | 2 +- shell/plugins/lock/LockView.qml | 2 +- shell/plugins/panels/weather/Panel.qml | 70 ++++++++++++++++----- test/cli | 16 +++++ themes/tokyo-night/shell.lock.toml | 6 ++ 9 files changed, 203 insertions(+), 33 deletions(-) create mode 100644 themes/tokyo-night/shell.lock.toml diff --git a/bin/omarchy-theme-set-templates b/bin/omarchy-theme-set-templates index 68ee0b49..620b84b7 100755 --- a/bin/omarchy-theme-set-templates +++ b/bin/omarchy-theme-set-templates @@ -290,6 +290,83 @@ add_gradient_function_values() { done } +strip_shell_section_header() { + local section="$1" + local file="$2" + + awk -v section="$section" ' + BEGIN { skipping_header = 0 } + /^[[:space:]]*\[[^]]+\][[:space:]]*($|#)/ { + if ($0 ~ "^[[:space:]]*\\[" section "\\][[:space:]]*($|#)") { + skipping_header = 1 + next + } + } + { print } + ' "$file" +} + +apply_shell_section_override() { + local override="$1" + local section tmp body + + [[ -f $NEXT_THEME_DIR/shell.toml ]] || return + [[ -f $override ]] || return + + section=$(basename "$override") + section="${section#shell.}" + section="${section%.toml}" + [[ $section =~ ^[A-Za-z0-9_-]+$ ]] || return + + tmp=$(mktemp) + body=$(mktemp) + + strip_shell_section_header "$section" "$override" >"$body" + + awk -v section="$section" -v body="$body" ' + function emit_override() { + if (emitted) return + print "[" section "]" + while ((getline line < body) > 0) print line + close(body) + emitted = 1 + } + + /^[[:space:]]*\[[^]]+\][[:space:]]*($|#)/ { + if (in_section) { + emit_override() + in_section = 0 + } + if ($0 ~ "^[[:space:]]*\\[" section "\\][[:space:]]*($|#)") { + in_section = 1 + next + } + } + + !in_section { print } + + END { + if (in_section || !emitted) { + if (NR > 0) print "" + emit_override() + } + } + ' "$NEXT_THEME_DIR/shell.toml" >"$tmp" + + mv "$tmp" "$NEXT_THEME_DIR/shell.toml" + rm "$body" +} + +apply_shell_section_overrides() { + local override + + shopt -s nullglob + for override in "$NEXT_THEME_DIR"/shell.*.toml; do + [[ $(basename "$override") != "shell.toml" ]] || continue + apply_shell_section_override "$override" + done +} + alias_theme_color() { local key="$1" local fallback="$2" @@ -426,3 +503,5 @@ if [[ -f $COLORS_FILE ]]; then rm "$sed_script" fi + +apply_shell_section_overrides diff --git a/docs/theming.md b/docs/theming.md index 733ad73a..7fb32dec 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -101,6 +101,19 @@ The helper does not choose the first color unless you use `gradient_start`. and bar sizing. The default generated file comes from `default/themed/shell.toml.tpl`. +Themes can override the entire generated file by shipping `shell.toml`, or just +one section by shipping `shell.
.toml`. For example, +`shell.lock.toml` replaces only the `[lock]` section after the default +`shell.toml` has been generated: + +```toml +text = "#ffffff" +placeholder = "#ffffff" +border = "#ffffff" +``` + +The filename decides the target section, so the `[lock]` header is optional. + The running shell reads `shell.toml` into two QML singletons: - `Color` for palette and surface roles like `Color.menu.border`. diff --git a/shell/Ui/KeyboardPanel.qml b/shell/Ui/KeyboardPanel.qml index 6d09093b..b861b8a2 100644 --- a/shell/Ui/KeyboardPanel.qml +++ b/shell/Ui/KeyboardPanel.qml @@ -14,11 +14,9 @@ import qs.Commons // their parent surface — so keyboard-summoned popups fell flat without it. // // API is a subset of Common.PopupCard: anchorItem, owner, bar, open, -// padding, margin, contentWidth/Height, default contentItem. Missing on -// purpose (for now): centerOnBar, triggerMode ("hover"), containsMouse. -// Hover-mode popups (system-stats, weather) and centered popups -// (calendar week-view) need extra plumbing before migrating; converting -// them is a follow-up. +// padding, margin, contentWidth/Height, centerOnBar, default contentItem. +// Missing on purpose (for now): triggerMode ("hover"), containsMouse. +// Hover-mode popups (system-stats) need extra plumbing before migrating. // // Positioning: full-screen layer-shell with the card placed inside at // `cardOrigin`. We use the bar window's height/width for the perpendicular @@ -42,6 +40,7 @@ PanelWindow { property int padding: Style.spacing.popupPadding property int contentWidth: Style.space(280) property int contentHeight: Style.space(200) + property bool centerOnBar: false property bool open: false property int gap: Style.gapsOut // distance between bar edge and panel property bool popoutSwitching: false @@ -173,7 +172,13 @@ PanelWindow { readonly property point cardOrigin: { if (!anchorItem || !bar) return Qt.point(margin, margin) var x = 0, y = 0 - if (barPos === "bottom") { + if (centerOnBar && (barPos === "top" || barPos === "bottom")) { + x = screenW / 2 - contentWidth / 2 + y = barPos === "bottom" ? screenH - barH - contentHeight - gap : barH + gap + } else if (centerOnBar) { + x = barPos === "left" ? barW + gap : screenW - barW - contentWidth - gap + y = screenH / 2 - contentHeight / 2 + } else if (barPos === "bottom") { x = anchorScreenPos.x + anchorW / 2 - contentWidth / 2 y = screenH - barH - contentHeight - gap } else if (barPos === "left") { diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index f54f46c1..4862202c 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -40,6 +40,8 @@ Item { property bool useTransparentForeground: false property bool transparent: false property bool centerSectionHovered: false + property bool centerSectionRevealHeld: false + property bool centerHoverRevealSuppressed: false property int barConfigSerial: 0 property string position: "top" // Resolves through fontconfig at paint time (Style.font.family defaults @@ -374,6 +376,22 @@ Item { Component.onCompleted: applyBarConfig() + function setCenterSectionHovered(hovered) { + centerSectionHovered = hovered + if (hovered) { + centerSectionRevealTimer.stop() + centerSectionRevealHeld = true + } else { + centerSectionRevealTimer.restart() + } + } + + Timer { + id: centerSectionRevealTimer + interval: 120 + onTriggered: root.centerSectionRevealHeld = root.centerSectionHovered + } + function run(command) { if (!command) return @@ -970,7 +988,7 @@ Item { CenterGestureArea { anchors.fill: parent } HoverHandler { - onHoveredChanged: root.centerSectionHovered = hovered + onHoveredChanged: root.setCenterSectionHovered(hovered) } ModuleList { @@ -1001,7 +1019,7 @@ Item { visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock" clockHovered: centerAnchorModule.hovered - centerHovered: root.centerSectionHovered + centerHovered: root.centerSectionRevealHeld && !root.centerHoverRevealSuppressed anchors.right: centerAnchorModule.left anchors.verticalCenter: centerAnchorModule.verticalCenter } @@ -1025,7 +1043,7 @@ Item { CenterGestureArea { anchors.fill: parent } HoverHandler { - onHoveredChanged: root.centerSectionHovered = hovered + onHoveredChanged: root.setCenterSectionHovered(hovered) } ModuleList { @@ -1056,7 +1074,7 @@ Item { visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock" clockHovered: centerAnchorModule.hovered - centerHovered: root.centerSectionHovered + centerHovered: root.centerSectionRevealHeld && !root.centerHoverRevealSuppressed anchors.bottom: centerAnchorModule.top anchors.horizontalCenter: centerAnchorModule.horizontalCenter } @@ -1098,13 +1116,8 @@ Item { implicitHeight: button.implicitHeight width: implicitWidth height: implicitHeight - opacity: revealed ? 1.0 : 0 z: 500 - Behavior on opacity { - NumberAnimation { duration: 120; easing.type: Easing.OutCubic } - } - HoverHandler { id: controlHover } Component.onCompleted: root.registerConfigControl(configControl) diff --git a/shell/plugins/bar/widgets/Indicators.qml b/shell/plugins/bar/widgets/Indicators.qml index 198ecc88..dde04ee9 100644 --- a/shell/plugins/bar/widgets/Indicators.qml +++ b/shell/plugins/bar/widgets/Indicators.qml @@ -17,7 +17,7 @@ BarWidget { property bool indicatorAreaHovered: false property bool indicatorItemHovered: false readonly property bool alwaysShowIndicators: setting("alwaysShow", false) === true - readonly property bool revealInactiveIndicators: alwaysShowIndicators || indicatorAreaHovered || indicatorItemHovered || (bar && bar.centerSectionHovered === true) + readonly property bool revealInactiveIndicators: alwaysShowIndicators || indicatorAreaHovered || indicatorItemHovered || (bar && bar.centerSectionRevealHeld === true && bar.centerHoverRevealSuppressed !== true) signal refreshRequested() diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml index cd723514..8853039b 100644 --- a/shell/plugins/lock/LockView.qml +++ b/shell/plugins/lock/LockView.qml @@ -28,7 +28,7 @@ Item { readonly property bool errorState: failureMessage.length > 0 readonly property var inputBorderSpec: errorState ? Border.surfaceSpec("lock", "border-error", Color.lock.borderError, root.outlineThickness, "border-alpha") - : Border.hyprlandActiveSpec(Color.accent, root.outlineThickness) + : Border.surfaceSpec("lock", "border-active", Color.lock.borderActive, root.outlineThickness, "border-alpha") signal submitPassword(string password) signal passwordTextEdited(string password) diff --git a/shell/plugins/panels/weather/Panel.qml b/shell/plugins/panels/weather/Panel.qml index b7abde40..8a73407f 100644 --- a/shell/plugins/panels/weather/Panel.qml +++ b/shell/plugins/panels/weather/Panel.qml @@ -9,17 +9,38 @@ Panel { id: root moduleName: "omarchy.weather" ipcTarget: "omarchy.weather" + manageIpc: false property var anchorItem: null + property bool openedFromHotkey: false function open() { + openedFromHotkey = false + setCenterHoverRevealSuppressed(false) root.controller.show() root.refresh() } + function openFromHotkey() { + openedFromHotkey = true + setCenterHoverRevealSuppressed(true) + root.controller.show() + root.refresh() + } + + function close() { + setCenterHoverRevealSuppressed(false) + root.controller.hide() + } + function toggle() { if (root.opened) root.close() - else root.open() + else root.openFromHotkey() + } + + function setCenterHoverRevealSuppressed(value) { + if (root.bar && "centerHoverRevealSuppressed" in root.bar) + root.bar.centerHoverRevealSuppressed = value } // Parsed wttr.in j1 response. Kept on failure so stale data stays visible. @@ -190,29 +211,45 @@ Panel { onTriggered: root.refresh() } - PopupCard { - id: popup + IpcHandler { + target: root.ipcTarget + + function open(): void { root.openFromHotkey() } + function close(): void { root.close() } + function show(): void { root.openFromHotkey() } + function hide(): void { root.close() } + function toggle(): void { root.toggle() } + } + + KeyboardPanel { + id: panel anchorItem: root.anchorItem owner: root bar: root.bar open: root.opened centerOnBar: true - triggerMode: "click" - contentWidth: popup.fittedContentWidth(Style.space(480)) - contentHeight: popup.fittedContentHeight(weatherColumn.implicitHeight) + focusTarget: keyCatcher + contentWidth: panel.fittedContentWidth(Style.space(480)) + contentHeight: panel.fittedContentHeight(weatherColumn.implicitHeight) - Flickable { - id: weatherScroll + PanelKeyCatcher { + id: keyCatcher anchors.fill: parent - contentWidth: width - contentHeight: weatherColumn.implicitHeight - clip: true - boundsBehavior: Flickable.StopAtBounds + onCloseRequested: root.close() + onTabRequested: function(direction) { root.switchPanel(direction) } - Column { - id: weatherColumn - width: weatherScroll.width - spacing: Style.space(14) + Flickable { + id: weatherScroll + anchors.fill: parent + contentWidth: width + contentHeight: weatherColumn.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + + Column { + id: weatherColumn + width: weatherScroll.width + spacing: Style.space(14) // ---- Hero row: big icon + temp on the left; location and stats stacked on the right. Item { @@ -431,6 +468,7 @@ Panel { } } } + } // Poll the weather pill text/class every minute. Local to this widget. Process { diff --git a/test/cli b/test/cli index f4ee5bb7..619e2683 100755 --- a/test/cli +++ b/test/cli @@ -264,6 +264,12 @@ bright-magenta={{ bright_magenta }} legacy-bright-purple={{ bright_purple }} EOF +cat >"$NEXT_THEME/shell.lock.toml" <<'EOF' +text = "#ffffff" +placeholder = "#ffffff" +border = "#ffffff" +EOF + OMARCHY_PATH="$ROOT" HOME="$PI_TMPDIR" "$ROOT/bin/omarchy-theme-set-templates" grep -qx 'mix=#808080' "$NEXT_THEME/mix-test.txt" || fail "theme template mix helper works" grep -qx 'strip=808080' "$NEXT_THEME/mix-test.txt" || fail "theme template mix_strip helper works" @@ -291,6 +297,16 @@ grep -qx 'fallback-shell=#336699' "$NEXT_THEME/gradient-test.txt" || fail "theme grep -qx 'fallback-shell-gradient=#336699' "$NEXT_THEME/gradient-test.txt" || fail "theme template shell_gradient fallback works" pass "theme template gradient helpers work" +awk ' + /^\[lock\]$/ { in_lock = 1; next } + /^\[/ { in_lock = 0 } + in_lock && /^text = "#ffffff"$/ { text = 1 } + in_lock && /^placeholder = "#ffffff"$/ { placeholder = 1 } + in_lock && /^border = "#ffffff"$/ { border = 1 } + END { exit(text && placeholder && border ? 0 : 1) } +' "$NEXT_THEME/shell.toml" || fail "theme shell section override is merged" +pass "theme shell section override is merged" + jq -e '.name == "omarchy-system" and .vars.panel == "#0f0f0f" and .vars.border == "#4d4d4d" and .colors.accent == "accent"' "$NEXT_THEME/pi.json" >/dev/null pass "pi theme template is generated from standard themed templates" cp "$NEXT_THEME/pi.json" "$CURRENT_THEME/pi.json" diff --git a/themes/tokyo-night/shell.lock.toml b/themes/tokyo-night/shell.lock.toml new file mode 100644 index 00000000..80720721 --- /dev/null +++ b/themes/tokyo-night/shell.lock.toml @@ -0,0 +1,6 @@ +text = "#a9b1d6" +placeholder = "#a9b1d6" +text-error = "#a9b1d6" +border = "#a9b1d6" +border-active = "#a9b1d6" +border-error = "#a9b1d6"