Replace bar settings with inline config panel

This commit is contained in:
David Heinemeier Hansson
2026-05-27 15:11:36 +02:00
parent eef5668d89
commit bc32980cca
24 changed files with 377 additions and 1788 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# omarchy:summary=Launch the Omarchy bar settings panel
# omarchy:summary=Open the Omarchy bar config panel
exec omarchy-shell shell summon omarchy.settings "{}"
exec omarchy-shell shell openBarConfig
+3 -3
View File
@@ -3,7 +3,7 @@
# omarchy:summary=Manage Omarchy shell plugins and bar widgets
# omarchy:group=plugin
# omarchy:args=<list|rescan|enable|disable|clone|edit|bar> [...]
# omarchy:examples=omarchy plugin list | omarchy plugin clone omarchy.clock local.clock --with ai | omarchy plugin edit local.clock --with editor | omarchy plugin enable acme.weather --section right | omarchy plugin bar edit
# omarchy:examples=omarchy plugin list | omarchy plugin clone omarchy.clock local.clock --with ai | omarchy plugin edit local.clock --with editor | omarchy plugin enable acme.weather --section right | omarchy plugin bar settings
set -euo pipefail
@@ -30,7 +30,7 @@ Clone options:
--add [placement] Add cloned bar widget to the bar
Bar widget commands:
bar edit Open the visual bar settings panel
bar settings Open the inline bar config panel
bar list [--json] List current bar layout
bar add <id> [placement] [--duplicate]
bar move <id> [placement] [--from-section SECTION] [--from-index N]
@@ -1136,7 +1136,7 @@ bar_command() {
case "$command" in
edit | settings)
(( $# == 0 )) || fail "bar edit does not take arguments"
(( $# == 0 )) || fail "bar settings does not take arguments"
exec omarchy-launch-bar-settings
;;
list)
+1 -1
View File
@@ -23,7 +23,7 @@ Options:
Examples:
omarchy-shell shell ping
omarchy-shell shell summon omarchy.settings "{}"
omarchy-shell shell openBarConfig
omarchy-shell -q Indicators refresh
omarchy-shell shell listPlugins
omarchy-shell shell toggle omarchy.menu '{"menu":"root"}'
+1 -18
View File
@@ -1,6 +1,5 @@
-- Window and layer rules for the Omarchy Quickshell surfaces. The
-- shell-wide bar / menu / popouts are layer-shell; the bar settings panel
-- is a regular Hyprland window via Quickshell's FloatingWindow.
-- shell-wide bar / menu / popouts are layer-shell.
-- Keep the bar instant: no layer-shell fade/slide animation.
hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation = "none" })
@@ -10,22 +9,6 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation
-- QML opacity transition for normal open/close, and skip it for panel handoff.
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-launcher|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel)$" }, no_anim = true, animation = "none" })
-- Bar settings floats centered with a sensible default size instead of
-- tiling — it's a transient dialog, not a workspace surface.
o.window({
class = "^org.quickshell$",
title = "^Omarchy Bar Settings$",
}, {
float = true,
center = true,
size = { 760, 620 },
})
-- Dev gallery is the main shell workbench; open it maximized like
-- SUPER+ALT+F so component previews have the whole workspace.
o.window({ class = "^org.quickshell$", title = "^Omarchy shell dev gallery$" }, { maximize = true })
-- Per-widget settings dialog opens as a smaller FloatingWindow off the
-- bar settings panel; keep it floating with its own default size.
o.window({ class = "^org.quickshell$", title = "^Widget settings " }, { float = true })
o.window({ class = "^org.quickshell$", title = "^Widget settings " }, { size = { 380, 320 } })
+2 -3
View File
@@ -26,7 +26,7 @@ first call.
| Kind | What it is |
|--------------|---------------------------------------------|
| `bar-widget` | Component the bar drops into a section |
| `panel` | Floating window (e.g. bar settings) |
| `panel` | Floating window (e.g. OSD) |
| `overlay` | Fullscreen overlay (e.g. background picker) |
| `menu` | Summoned menu surface |
| `service` | Headless singleton, no UI |
@@ -43,8 +43,7 @@ Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegis
plus the QML referenced from `entryPoints`.
2. `omarchy plugin rescan`
3. `omarchy plugin enable <id>`
4. Bar widgets also need adding to a section with `omarchy plugin bar add <id>`
or the visual editor (`omarchy plugin bar edit`).
4. Bar widgets also need adding to a section with `omarchy plugin bar add <id>`.
The lower-level IPC methods remain available through `omarchy-shell shell ...`
for callers that need to talk directly to the running shell.
+1 -1
View File
@@ -83,7 +83,7 @@ QtObject {
}
}
// Layout normalization shared by the bar host and the bar settings panel
// Layout normalization shared by bar config consumers
// so the two never drift. Entries are deep-cloned to decouple from the
// input config; consumers can mutate without leaking back to shell.json.
function normalizeLayoutEntry(entry) {
+6 -11
View File
@@ -2,9 +2,8 @@
`omarchy-shell` is a single long-running [Quickshell](https://quickshell.org/)
instance that hosts the Omarchy desktop. Hyprland autostart launches one shell
per graphical session; everything else — the bar, the bar settings UI, the
background switcher, future panels and overlays — runs **inside** the shell as
a plugin.
per graphical session; everything else — the bar, background switcher, panels,
and overlays — runs **inside** the shell as a plugin.
Hosting everything inside one shell means:
@@ -24,7 +23,6 @@ shell/
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
plugins/
bar/ first-party plugins (see plugins/README.md)
settings/ settings panel + plugin-declared widget forms
launcher/
image-picker/
menu/
@@ -77,7 +75,7 @@ Supported `kinds`:
| Kind | What it is |
|--------------|--------------------------------------------------------------|
| `bar-widget` | A component that the bar can drop into a section |
| `panel` | A persistent or summoned floating window (e.g. bar settings) |
| `panel` | A persistent or summoned floating window (e.g. OSD) |
| `overlay` | A fullscreen overlay (e.g. background switcher) |
| `menu` | A summoned menu surface |
| `service` | A headless singleton, no UI |
@@ -97,8 +95,7 @@ The full schema lives in `services/PluginRegistry.qml`.
referenced from its `entryPoints`.
2. `omarchy plugin rescan`.
3. Enable the plugin with `omarchy plugin enable <id>`.
4. If it's a `bar-widget`, place it with `omarchy plugin bar add <id>` or
open the visual editor with `omarchy plugin bar edit`.
4. If it's a `bar-widget`, place it with `omarchy plugin bar add <id>`.
The lower-level IPC equivalents are still available via `omarchy-shell shell rescanPlugins`,
`omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`.
@@ -153,7 +150,7 @@ calls to the running shell. It does not start the shell.
```
omarchy-shell shell ping
omarchy-shell shell summon omarchy.settings "{}"
omarchy-shell shell openBarConfig
omarchy-shell shell toggle omarchy.menu '{"menu":"root"}'
omarchy-shell shell listPlugins
omarchy-shell shell rescanPlugins
@@ -177,9 +174,7 @@ customization from the shipped defaults lives in it.
The `config/omarchy/shell.json` default config describes the
fresh-install state. When the user has no `shell.json`, the shell uses
the defaults verbatim. Once the user customizes anything, `shell.json`
becomes the authoritative file — we do **not** deep-merge defaults back
in. Pressing **Reset bar to defaults** in `omarchy launch bar settings`
rewrites the `bar` subtree from the current default shell config.
becomes the authoritative file — we do **not** deep-merge defaults back in.
### shell.json shape
+1 -1
View File
@@ -332,7 +332,7 @@ Item {
QQC.Popup {
id: popup
// Reparent to the window's content item so the popup is free of any
// clipping ancestor (e.g. the bar settings dialog Flickable). Position
// clipping ancestor. Position
// and available height are recomputed on open and any time the
// trigger's geometry changes, since a binding on mapToItem alone
// won't reliably re-evaluate when ancestors scroll or resize.
+1 -1
View File
@@ -15,7 +15,7 @@ import qs.Commons
//
// Set `focusable: true` to make the button keyboard-tabbable with the
// shared hover-cursor/focus tokens. Use this
// in form contexts (the bar settings widget cards) where Tab walks a list
// in form contexts where Tab walks a list
// of controls; leave it false for the right-edge actions on panel rows
// where the row's CursorSurface owns the keyboard cursor.
//
+1 -1
View File
@@ -5,7 +5,7 @@ import qs.Commons
// Searchable single-select dropdown. Same trigger shape as Dropdown, but
// the popup leads with an embedded TextField that filters the option
// list in real time. Use for pickers with enough options that scanning
// is friction (e.g. bar settings "+ Add widget").
// is friction.
//
// Filtering is case-insensitive substring against each option's label.
// Options can be string[] or [{ value, label, description? }] — the same
-12
View File
@@ -12,7 +12,6 @@ User-installed plugins live alongside these conceptually but on disk under
| Plugin | id | kinds | entry point |
|---------------|---------------------------|-------------------------|---------------------------------------|
| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` |
| Bar settings | `omarchy.settings` | `panel` | `settings/SettingsPanel.qml` |
| Launcher | `omarchy.launcher` | `overlay` | `launcher/Launcher.qml` |
| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` |
| Emojis | `omarchy.emojis` | `overlay` | `emojis/Emojis.qml` |
@@ -46,17 +45,6 @@ providing [`config/omarchy/shell.json`](../../config/omarchy/shell.json) when
the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue
and customization schema.
## Bar settings
Visual editor for the bar layout. Summoned by
`omarchy-shell shell summon omarchy.settings "{}"` (which is what
`omarchy launch bar settings` ultimately calls). Provides:
- bar position and center-anchor controls
- per-section add/move/remove/edit of bar widget entries
- dynamic per-widget settings forms that write inline back to the
corresponding shell.json entry
## Launcher
Quickshell-powered launcher. It uses Quickshell's native
+100 -19
View File
@@ -12,8 +12,7 @@ Item {
// The omarchy-shell host injects omarchyPath from OMARCHY_PATH.
required property string omarchyPath
// Injected by the host shell. Shared with the bar settings panel so both
// see the same widget catalogue.
// Injected by the host shell so bar slots can resolve enabled widgets.
required property var barWidgetRegistry
// Injected by the host shell every time shell.json is reloaded. Holds the
// `bar:` subtree: position, centerAnchor, layout. The host owns file IO;
@@ -69,6 +68,7 @@ Item {
property var activePopout: null
property var barDragTarget: null
property bool barDragAfter: false
property var configControls: []
property var clickTargets: []
property var debugModuleSlots: []
@@ -96,6 +96,18 @@ Item {
debugModuleSlots = next
}
function registerConfigControl(control) {
if (!control || configControls.indexOf(control) !== -1) return
var next = configControls.slice()
next.push(control)
configControls = next
}
function unregisterConfigControl(control) {
var next = configControls.filter(function(item) { return item !== control })
configControls = next
}
function debugBarGeometry() {
var out = []
for (var i = 0; i < debugModuleSlots.length; i++) {
@@ -164,7 +176,7 @@ Item {
}
// Apply tray-pinning on top of the shared layout normalization so the
// bar host and the bar settings panel can't drift on entry shape.
// bar host and scriptable config helpers can't drift on entry shape.
function normalizeLayout(layout) {
var normalized = Util.normalizeLayout(Util.isPlainObject(layout) ? layout : fallbackBarConfig.layout)
return {
@@ -305,12 +317,14 @@ Item {
launcher.startDetached()
}
function openBarSettings() {
if (root.shell && typeof root.shell.summon === "function") {
root.shell.summon("omarchy.settings", "{}")
} else {
root.run("omarchy-launch-bar-settings")
function openConfigPanel() {
for (var i = 0; i < configControls.length; i++) {
var control = configControls[i]
if (!control || control.visible !== true || typeof control.openPanel !== "function") continue
control.openPanel()
return true
}
return false
}
function toggleTransparency() {
@@ -821,7 +835,7 @@ Item {
visible: centerRoot.hasAnchor
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
region: "center"
anchors.right: centerAnchorModule.left
anchors.right: centerConfigControl.visible ? centerConfigControl.left : centerAnchorModule.left
anchors.verticalCenter: centerAnchorModule.verticalCenter
}
@@ -833,6 +847,15 @@ Item {
anchors.centerIn: parent
}
BarConfigControl {
id: centerConfigControl
visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock"
clockHovered: centerAnchorModule.hovered
anchors.right: centerAnchorModule.left
anchors.verticalCenter: centerAnchorModule.verticalCenter
}
ModuleList {
visible: centerRoot.hasAnchor
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
@@ -862,7 +885,7 @@ Item {
visible: centerRoot.hasAnchor
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
region: "center"
anchors.bottom: centerAnchorModule.top
anchors.bottom: centerConfigControl.visible ? centerConfigControl.top : centerAnchorModule.top
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
@@ -874,6 +897,15 @@ Item {
anchors.centerIn: parent
}
BarConfigControl {
id: centerConfigControl
visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock"
clockHovered: centerAnchorModule.hovered
anchors.bottom: centerAnchorModule.top
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
ModuleList {
visible: centerRoot.hasAnchor
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
@@ -886,23 +918,69 @@ Item {
}
component CenterGestureArea: MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
root.openBarSettings()
mouse.accepted = true
}
}
acceptedButtons: Qt.LeftButton
onDoubleClicked: function(mouse) {
if (mouse.button !== Qt.RightButton) {
if (mouse.button === Qt.LeftButton) {
root.toggleTransparency()
mouse.accepted = true
}
}
}
component BarConfigControl: Item {
id: configControl
property bool clockHovered: false
readonly property bool panelOpen: configPanel.opened
readonly property bool revealed: visible && (clockHovered || controlHover.hovered || panelOpen)
implicitWidth: button.implicitWidth
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)
Component.onDestruction: root.unregisterConfigControl(configControl)
function openPanel() {
configPanel.open()
}
WidgetButton {
id: button
anchors.fill: parent
bar: root
text: ""
keepSpace: true
concealed: !configControl.revealed
interactive: configControl.revealed
horizontalMargin: 6.5
verticalPadding: 6
tooltipText: "Bar config"
onPressed: function(b) {
if (b === Qt.LeftButton) configPanel.toggle()
}
}
BarConfigPanel {
id: configPanel
bar: root
anchorItem: button
}
}
component ModuleList: Loader {
id: moduleListRoot
@@ -976,6 +1054,7 @@ Item {
if (qmlCustom) return qmlLoader.item
return componentLoader.item
}
readonly property bool hovered: moduleHover.hovered
implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
@@ -986,6 +1065,8 @@ Item {
Component.onCompleted: root.registerDebugModuleSlot(slot)
Component.onDestruction: root.unregisterDebugModuleSlot(slot)
HoverHandler { id: moduleHover }
Loader {
id: componentLoader
active: !slot.qmlCustom && !slot.registered
+246
View File
@@ -0,0 +1,246 @@
import QtQuick
import qs.Commons
import qs.Ui
Panel {
id: root
moduleName: "omarchy.bar-config"
manageIpc: false
property Item anchorItem: null
property string focusSection: "transparency"
property int positionIndex: 0
property bool cursorActive: false
property int phraseIndex: 0
readonly property string currentPosition: bar ? bar.position : "top"
readonly property bool transparent: bar ? bar.requestedTransparent === true : false
readonly property color foreground: bar ? bar.foreground : Color.foreground
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
readonly property var heroPhrases: [
"Picking Sides",
"Choose Transparency",
"Edge Decisions",
"Bar Exam",
"Top Shelf Thinking",
"Side Quest Settings"
]
readonly property string heroPhraseText: heroPhrases[phraseIndex % heroPhrases.length]
readonly property var positionOptions: [
{ value: "top", label: "Top" },
{ value: "bottom", label: "Bottom" },
{ value: "left", label: "Left" },
{ value: "right", label: "Right" }
]
function normalizePosition(value) {
var next = String(value || "")
return /^(top|bottom|left|right)$/.test(next) ? next : "top"
}
function positionLabel(value) {
var next = normalizePosition(value)
return next.charAt(0).toUpperCase() + next.slice(1)
}
function currentPositionIndex() {
var current = normalizePosition(currentPosition)
for (var i = 0; i < positionOptions.length; i++)
if (positionOptions[i].value === current) return i
return 0
}
function mutateBarConfig(mutator) {
if (!bar || !bar.shell || typeof bar.shell.mutateShellConfig !== "function") return false
bar.shell.mutateShellConfig(function(config) {
if (!Util.isPlainObject(config.bar)) config.bar = {}
mutator(config.bar)
})
return true
}
function setTransparency(value) {
var next = value === true
if (mutateBarConfig(function(barConfig) { barConfig.transparent = next })) return
if (bar && typeof bar.setRequestedTransparency === "function") bar.setRequestedTransparency(next)
}
function setPosition(value) {
var next = normalizePosition(value)
if (mutateBarConfig(function(barConfig) { barConfig.position = next })) return
if (bar) bar.position = next
}
function moveCursor(dx, dy) {
if (!cursorActive) {
cursorActive = true
return
}
if (dy !== 0) {
focusSection = focusSection === "transparency" ? "position" : "transparency"
if (focusSection === "position") positionIndex = currentPositionIndex()
return
}
if (dx !== 0 && focusSection === "position") {
var count = positionOptions.length
positionIndex = (positionIndex + (dx > 0 ? 1 : -1) + count) % count
}
}
function activateCursor() {
if (focusSection === "transparency") {
setTransparency(!transparent)
return
}
var option = positionOptions[positionIndex]
if (option) setPosition(option.value)
}
onOpenedChanged: {
if (!opened) return
focusSection = "transparency"
positionIndex = currentPositionIndex()
cursorActive = false
}
onCurrentPositionChanged: if (!cursorActive || focusSection === "position") positionIndex = currentPositionIndex()
Timer {
id: phraseTimer
interval: 2200
running: root.opened
repeat: true
onTriggered: phraseSwap.restart()
}
SequentialAnimation {
id: phraseSwap
PropertyAnimation {
target: hero; property: "metaOpacity"
to: 0.0; duration: 180; easing.type: Easing.OutQuad
}
ScriptAction {
script: root.phraseIndex = (root.phraseIndex + 1) % root.heroPhrases.length
}
PropertyAnimation {
target: hero; property: "metaOpacity"
to: 1.0; duration: 260; easing.type: Easing.InQuad
}
}
Component {
id: heroIconComponent
Text {
text: ""
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.display
}
}
KeyboardPanel {
id: panel
anchorItem: root.anchorItem
owner: root
bar: root.bar
open: root.opened && root.anchorItem !== null
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(380))
contentHeight: panel.fittedContentHeight(contentColumn.implicitHeight)
PanelKeyCatcher {
id: keyCatcher
anchors.fill: parent
onMoveRequested: function(dx, dy) { root.moveCursor(dx, dy) }
onActivateRequested: root.activateCursor()
onCloseRequested: root.close()
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Style.space(14)
PanelHero {
id: hero
width: parent.width
iconComponent: heroIconComponent
title: "Bar"
meta: root.heroPhraseText
foreground: root.foreground
fontFamily: root.fontFamily
}
Column {
width: parent.width
spacing: Style.space(8)
PanelSectionHeader {
text: "APPEARANCE"
foreground: root.foreground
fontFamily: root.fontFamily
}
Toggle {
width: parent.width
label: "Transparency"
description: root.transparent ? "Wallpaper visible" : "Solid background"
checked: root.transparent
hasCursor: root.cursorActive && root.focusSection === "transparency"
foreground: root.foreground
accent: Color.accent
fontFamily: root.fontFamily
onClicked: root.setTransparency(!root.transparent)
onHovered: function(h) {
if (!h) return
root.cursorActive = true
root.focusSection = "transparency"
}
}
}
Column {
width: parent.width
spacing: Style.space(8)
PanelSectionHeader {
text: "POSITION"
foreground: root.foreground
fontFamily: root.fontFamily
}
ButtonGroup {
id: positionGroup
width: parent.width
options: root.positionOptions
value: root.currentPosition
foreground: root.foreground
background: "transparent"
accent: Color.accent
fontFamily: root.fontFamily
focusable: false
cursorIndex: root.cursorActive && root.focusSection === "position" ? root.positionIndex : -1
onChanged: function(value) { root.setPosition(value) }
onHovered: function(index, hovered) {
if (!hovered) return
root.cursorActive = true
root.focusSection = "position"
root.positionIndex = index
}
}
}
}
}
}
}
+2 -2
View File
@@ -14,9 +14,9 @@ the shell for its whole session.
## Customizing
The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`config/omarchy/shell.json`](../../../config/omarchy/shell.json). Once you customize anything via `omarchy launch bar settings` or by editing shell.json directly, your file is canonical — there is no deep-merge.
The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`config/omarchy/shell.json`](../../../config/omarchy/shell.json). Once you customize anything via the inline bar config panel, `omarchy plugin bar ...`, or by editing shell.json directly, your file is canonical — there is no deep-merge.
Launch the visual editor with `omarchy launch bar settings` / `omarchy plugin bar edit` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand. For scriptable changes, use `omarchy plugin bar list`, `omarchy plugin bar add`, `omarchy plugin bar move`, `omarchy plugin bar remove`, and `omarchy plugin bar set`. You can also right-click empty space to the left or right of the centered clock module to open it; double-left-click the same empty space to toggle bar transparency.
Open quick position and transparency controls with `omarchy launch bar settings` / `omarchy plugin bar settings` (or run `omarchy-launch-bar-settings`). You can also hover the centered clock module to reveal the inline bar config button. For scriptable widget changes, use `omarchy plugin bar list`, `omarchy plugin bar add`, `omarchy plugin bar move`, `omarchy plugin bar remove`, and `omarchy plugin bar set`. Double-left-click empty center-bar space to toggle bar transparency.
Example `shell.json` (bar subtree only shown):
+1 -1
View File
@@ -1550,7 +1550,7 @@ Item {
font.bold: true
}
Text {
text: "Dropdown with an embedded filter input. Type to narrow the list, Down to jump from the search to the first match, Enter to select. Use this for the bar settings \"+ Add widget\" picker and any other long-list selector."
text: "Dropdown with an embedded filter input. Type to narrow the list, Down to jump from the search to the first match, Enter to select. Use this for long option lists where plain scrolling is friction."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
+1 -1
View File
@@ -29,7 +29,7 @@ Item {
// ~/.cache where regeneratable artifacts belong.
readonly property string cacheDir: home + "/.cache/omarchy/"
readonly property string imageCacheDir: cacheDir + "notification-images/"
// Corner radius is shared with the menu and bar settings panel.
// Corner radius is shared with the menu and shell panels.
// It mirrors Hyprland's current decoration:rounding value.
readonly property int cornerRadius: Style.cornerRadius
// Surfaces anchor relative to the omarchy bar so popups and history land
+1 -1
View File
@@ -35,4 +35,4 @@ Renders the Tailscale mark natively as a theme-colored 3×3 dot grid, matching t
## Add to the bar
This widget ships as first-party plugin `omarchy.tailscale`. Add it through Omarchy's bar settings UI, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json` and restart the shell.
This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy plugin bar add omarchy.tailscale`, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json` and restart the shell.
File diff suppressed because it is too large Load Diff
@@ -1,281 +0,0 @@
import QtQuick
import QtQuick.Controls as QQC
import qs.Commons
import qs.Ui as Ui
Column {
id: root
signal fieldChanged(string key, var value)
property var schema: []
property var entry: ({})
property color foreground: Color.popups.text
property color accent: Color.accent
property string fontFamily: Style.font.family
// Resolved on disk path of the plugin that owns this form, used to
// resolve relative argv entries in a multiselect's `optionsCommand`.
property string pluginSourceDir: ""
spacing: Style.spacing.xl
width: parent ? parent.width : 0
function currentValue(field) {
if (entry && entry[field.key] !== undefined) return entry[field.key]
if (field.defaultValue !== undefined) return field.defaultValue
switch (field.type) {
case "boolean": return false
case "integer":
case "number": return field.min !== undefined ? field.min : 0
case "enum": return field.options && field.options.length > 0 ? field.options[0] : ""
case "multiselect": return []
default: return ""
}
}
// Resolve a multiselect's optionsCommand argv against the plugin's source
// directory. The first entry is treated as a path relative to the plugin
// dir unless it is absolute or begins with ".". Subsequent entries pass
// through unchanged.
// Resolve a multiselect's optionsCommand argv against the plugin's source
// directory. The first argv entry must be a relative path inside the
// plugin dir (no leading `/`, no `..` segments, no empty segments);
// anything else is rejected and the field falls back to whatever static
// `options` it has. Subsequent argv entries pass through unchanged. We
// avoid `Array.isArray` because schema-supplied arrays sometimes arrive
// as QML JSValue lists that don't satisfy it; iterating by `.length`
// works for both real arrays and JSValue lists.
function resolveOptionsCommand(field) {
var oc = field ? field.optionsCommand : undefined
if (!oc || typeof oc.length !== "number" || oc.length === 0) return []
var argv = []
for (var i = 0; i < oc.length; i++) argv.push(String(oc[i]))
if (!pluginSourceDir) return []
var head = argv[0]
if (head.length === 0 || head.charAt(0) === "/") {
console.warn("DynamicSettingsForm: optionsCommand must be a path relative to the plugin dir, got: " + head)
return []
}
var rel = head.replace(/^\.\//, "")
var segments = rel.split("/")
for (var s = 0; s < segments.length; s++) {
if (segments[s] === ".." || segments[s] === "") {
console.warn("DynamicSettingsForm: optionsCommand may not contain '..' or empty segments: " + head)
return []
}
}
var dir = pluginSourceDir.replace(/\/$/, "")
argv[0] = dir + "/" + rel
return argv
}
Repeater {
model: root.schema
Column {
required property var modelData
width: root.width
spacing: Style.spacing.labelGap
Text {
visible: text !== "" && String(modelData && modelData.type ? modelData.type : "") !== "boolean"
text: modelData && modelData.label ? modelData.label : (modelData && modelData.key ? modelData.key : "")
color: Qt.darker(root.foreground, 1.3)
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Text {
visible: !!(modelData && modelData.description) && String(modelData && modelData.type ? modelData.type : "") !== "boolean"
text: modelData ? (modelData.description || "") : ""
color: Qt.darker(root.foreground, 1.6)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
width: parent.width
}
Loader {
width: parent.width
height: item ? item.implicitHeight : 0
sourceComponent: {
if (!modelData || !modelData.type) return stringField
switch (String(modelData.type)) {
case "boolean": return booleanField
case "enum": return enumField
case "integer": return integerField
case "number": return numberField
case "multiselect": return multiselectField
default: return stringField
}
}
onLoaded: {
if (!item) return
item.fieldKey = modelData.key
item.field = modelData
}
}
}
}
Text {
visible: !root.schema || root.schema.length === 0
text: "No settings."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
}
Component {
id: stringField
Ui.TextField {
property string fieldKey: ""
property var field: ({})
width: parent.width
foreground: root.foreground
accent: root.accent
font.family: root.fontFamily
font.pixelSize: Style.font.body
text: root.currentValue(field) === undefined ? "" : String(root.currentValue(field))
onEditingFinished: if (fieldKey) root.fieldChanged(fieldKey, text)
}
}
Component {
id: booleanField
Ui.Toggle {
property string fieldKey: ""
property var field: ({})
width: parent.width
label: field && field.label ? String(field.label) : "Enabled"
description: field && field.description ? String(field.description) : ""
foreground: root.foreground
accent: root.accent
fontFamily: root.fontFamily
checked: !!root.currentValue(field)
onClicked: if (fieldKey) root.fieldChanged(fieldKey, !checked)
}
}
Component {
id: enumField
Ui.Dropdown {
property string fieldKey: ""
property var field: ({})
width: parent.width
showLabel: false
foreground: root.foreground
accent: root.accent
fontFamily: root.fontFamily
options: field && field.options ? field.options : []
value: String(root.currentValue(field))
onChanged: function(value) {
if (fieldKey) root.fieldChanged(fieldKey, value)
}
}
}
Component {
id: integerField
Ui.NumberField {
property string fieldKey: ""
property var field: ({})
foreground: root.foreground
accent: root.accent
fontFamily: root.fontFamily
from: field && field.min !== undefined ? field.min : 0
to: field && field.max !== undefined ? field.max : 9999
stepSize: field && field.step !== undefined ? field.step : 1
value: {
var v = root.currentValue(field)
var n = typeof v === "number" ? v : parseInt(String(v || 0), 10)
return isFinite(n) ? n : 0
}
onModified: function(value) { if (fieldKey) root.fieldChanged(fieldKey, value) }
}
}
Component {
id: multiselectField
Ui.MultiSelect {
property string fieldKey: ""
property var field: ({})
width: parent.width
foreground: root.foreground
accent: root.accent
fontFamily: root.fontFamily
showLabel: false
// MultiSelect's arrayFrom() tolerates JSValue-style schema arrays, so
// we can hand it `field.options` directly without an Array.isArray
// guard that would silently drop JSValue lists.
options: field ? (field.options || []) : []
optionsCommand: root.resolveOptionsCommand(field)
optionsCommandCwd: root.pluginSourceDir
placeholderText: field && field.placeholderText ? String(field.placeholderText) : "Search..."
emptyText: field && field.emptyText ? String(field.emptyText) : "No options"
noSelectionText: field && field.noSelectionText ? String(field.noSelectionText) : "None selected"
values: {
var v = root.currentValue(field)
return v ? v : []
}
onChanged: function(arr) { if (fieldKey) root.fieldChanged(fieldKey, arr) }
}
}
Component {
id: numberField
Row {
property string fieldKey: ""
property var field: ({})
property real currentNumber: {
var v = root.currentValue(field)
var n = typeof v === "number" ? v : parseFloat(String(v || 0))
return isFinite(n) ? n : 0
}
width: parent.width
spacing: Style.spacing.rowGap
QQC.Slider {
id: slider
width: parent.width - readout.width - Style.spacing.rowGap
from: field && field.min !== undefined ? field.min : 0
to: field && field.max !== undefined ? field.max : 1
stepSize: field && field.step !== undefined ? field.step : 0.01
value: parent.currentNumber
onMoved: if (parent.fieldKey) root.fieldChanged(parent.fieldKey, value)
}
Text {
id: readout
text: slider.value.toFixed(2)
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
@@ -1,143 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
// Themed ComboBox + popup. Anchors below the trigger, paints with the host
// shell's foreground/background palette, and inherits the shell-wide corner
// radius so nothing renders rounded when the user has set sharp corners.
Item {
id: root
property string label: ""
property string value: ""
property var options: []
property color foreground: Color.popups.text
property color background: Color.popups.background
property color accent: Color.accent
property string fontFamily: Style.font.family
property int cornerRadius: 0
property int rowHeight: Style.spacing.controlHeight
property int popupRowHeight: Style.spacing.popupRowHeight
property bool showLabel: true
signal changed(string value)
implicitWidth: Style.spacing.dropdownWidth
implicitHeight: showLabel ? rowHeight + Style.spacing.huge : rowHeight
Column {
anchors.fill: parent
spacing: Style.spacing.labelGap
Text {
visible: root.showLabel && root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
}
ComboBox {
id: combo
width: parent.width
height: root.rowHeight
font.family: root.fontFamily
font.pixelSize: Style.font.body
model: root.options
currentIndex: {
for (var i = 0; i < model.length; i++) if (model[i] === root.value) return i
return -1
}
onActivated: function(index) {
if (index >= 0 && index < model.length) root.changed(model[index])
}
background: Rectangle {
color: combo.activeFocus
? Style.focusFillFor(root.foreground, root.accent)
: (combo.hovered
? Style.hoverFillFor(root.foreground, root.accent)
: Style.normalFillFor(root.foreground, root.accent))
border.color: combo.activeFocus
? Style.focusBorderFor(root.foreground, root.accent)
: (combo.hovered
? Style.hoverBorderFor(root.foreground, root.accent)
: Style.normalBorderFor(root.foreground, root.accent))
border.width: combo.activeFocus
? Style.focusBorderWidth
: (combo.hovered ? Style.hoverBorderWidth : Style.normalBorderWidth)
radius: root.cornerRadius
}
contentItem: Text {
leftPadding: Style.spacing.controlGap
rightPadding: Style.space(24)
text: combo.displayText
color: root.foreground
font: combo.font
verticalAlignment: Text.AlignVCenter
}
indicator: Text {
x: combo.width - width - Style.spacing.controlGap
y: combo.topPadding + (combo.availableHeight - height) / 2
text: "▾"
color: Qt.darker(root.foreground, 1.2)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
}
popup: Popup {
// Anchor the popup directly under the field, full width, sharp/round
// matching the shell radius. Override the native white-with-blue look.
y: combo.height
width: combo.width
implicitHeight: Math.min(contentItem.implicitHeight, root.popupRowHeight * 8)
padding: Style.spacing.hairline
background: Rectangle {
color: root.background
border.color: Style.normalBorderFor(root.foreground, root.accent)
border.width: Style.normalBorderWidth
radius: root.cornerRadius
}
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: combo.delegateModel
currentIndex: combo.highlightedIndex
boundsBehavior: Flickable.StopAtBounds
}
}
delegate: ItemDelegate {
required property var modelData
required property int index
width: combo.width
height: root.popupRowHeight
padding: 0
contentItem: Text {
text: String(modelData)
color: index === combo.highlightedIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
leftPadding: Style.spacing.controlPaddingX
rightPadding: Style.spacing.controlPaddingX
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: index === combo.highlightedIndex
? Style.hoverFillFor(root.foreground, root.accent)
: "transparent"
radius: 0
}
}
}
}
}
-14
View File
@@ -1,14 +0,0 @@
{
"schemaVersion": 1,
"id": "omarchy.settings",
"name": "Omarchy Bar Settings",
"version": "1.0.0",
"author": "Omarchy",
"description": "Customize the Omarchy bar position and widgets",
"kinds": [
"panel"
],
"entryPoints": {
"panel": "SettingsPanel.qml"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import QtQuick
// Instance, not a singleton instantiated once by shell.qml and injected into
// plugins that need to read or extend the widget catalogue. Relative-path
// singleton imports were creating per-importer instances which prevented the
// bar settings panel from seeing what the bar registered.
// shell host from seeing what the bar registered.
QtObject {
id: registry
+4
View File
@@ -651,6 +651,10 @@ ShellRoot {
return JSON.stringify(shell.bar && shell.bar.debugBarGeometry ? shell.bar.debugBarGeometry() : [])
}
function openBarConfig(): string {
return shell.bar && shell.bar.openConfigPanel && shell.bar.openConfigPanel() ? "ok" : "unknown"
}
function summon(id: string, payloadJson: string): string {
return shell.summon(id, payloadJson) ? "ok" : "unknown"
}
+2 -3
View File
@@ -95,7 +95,7 @@ done
jq -e '
map(.id) as $ids |
all(["omarchy.menu", "omarchy.settings", "omarchy.notifications", "omarchy.clock"][]; $ids | index(.)) and
all(["omarchy.menu", "omarchy.notifications", "omarchy.clock", "omarchy.osd"][]; $ids | index(.)) and
all(.[]; (.kinds | type == "array") and (.enabled | type == "boolean") and (.firstParty | type == "boolean"))
' <<<"$plugins" >/dev/null || {
printf 'Plugins:\n%s\n' "$plugins" | jq . >&2
@@ -115,8 +115,7 @@ jq -e '
}
pass "shell IPC returns effective shell config"
[[ $(shell_ipc shell summon omarchy.settings "{}") == "ok" ]] || fail_with_log "shell IPC summons settings panel"
shell_ipc_quiet shell hide omarchy.settings >/dev/null
[[ $(shell_ipc shell openBarConfig) == "ok" ]] || fail_with_log "shell IPC opens bar config panel"
[[ $(shell_ipc shell summon omarchy.launcher '{"query":"term"}') == "ok" ]] || fail_with_log "shell IPC summons launcher overlay"
shell_ipc_quiet shell hide omarchy.launcher >/dev/null
[[ $(shell_ipc shell summon missing.plugin "{}") == "unknown" ]] || fail_with_log "shell IPC rejects unknown plugin"