Add visual bar customizer
Each layout entry is now an object — {id, ...inline settings} — instead
of a (name, separate modules map) pair. Multiple instances of the same
widget are trivially supported (spacer is the main beneficiary). Settings
travel with the entry, so reordering keeps them attached.
shell.qml:
- normalizeLayout converts string entries to {id} and drops invalid ones
- ModuleSlot binds to a full entry; moduleName and moduleSettings derive
from it via entryId/entrySettings helpers
- centerAnchor lookup walks layoutEntries directly
- builtinBarConfig defaults use the object form
- README + bar-defaults.json updated to the new schema
bar-settings/ (new Quickshell config):
- Top toolbar with Reset / Save and Position / centerAnchor dropdowns
- Three section editors (left/center/right) with current widgets shown
as cards: name + description + move up/down/settings/remove buttons
- '+ Add widget' menu per section, sorted alphabetically by display name
(only spacer can repeat; everything else is one per bar)
- Per-widget settings dialog (FloatingWindow) loads an inline schema for
widgets in the catalog. Initial schemas: spacer (size), calendar
(formats), brightness (step)
- FileView { atomicWrites } writes to ~/.config/omarchy/bar.json on Save
omarchy-launch-bar-settings + hypr window rules pin the settings window
to a 760px floating square. controlCenter gains a 'Customize bar…' button
that launches it.
This commit is contained in:
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Launch the Omarchy bar customizer
|
||||
# omarchy:group=launch
|
||||
# omarchy:name=bar-settings
|
||||
# omarchy:examples=omarchy launch bar-settings
|
||||
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}"
|
||||
CONFIG_DIR="$OMARCHY_PATH/default/quickshell/bar-settings"
|
||||
|
||||
if quickshell list -p "$CONFIG_DIR" 2>/dev/null | grep -q '^Instance '; then
|
||||
hyprctl dispatch focuswindow 'title:Omarchy bar settings' >/dev/null 2>&1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$CONFIG_DIR" >/dev/null 2>&1 &
|
||||
@@ -0,0 +1,4 @@
|
||||
hl.window_rule({ match = { title = "^Omarchy bar settings$" }, tag = "+floating-window" })
|
||||
hl.window_rule({ match = { title = "^Omarchy bar settings$" }, size = { 760, 760 } })
|
||||
hl.window_rule({ match = { title = "^Widget settings " }, tag = "+floating-window" })
|
||||
hl.window_rule({ match = { title = "^Widget settings " }, size = { 420, 360 } })
|
||||
@@ -0,0 +1,882 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
property string home: Quickshell.env("HOME")
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH") || (home + "/.local/share/omarchy")
|
||||
readonly property string userConfigPath: home + "/.config/omarchy/bar.json"
|
||||
readonly property string defaultsPath: omarchyPath + "/default/quickshell/bar/bar-defaults.json"
|
||||
|
||||
property color foreground: "#cacccc"
|
||||
property color background: "#101315"
|
||||
property color accent: "#cacccc"
|
||||
property color urgent: "#a55555"
|
||||
|
||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
|
||||
property var defaultConfig: ({})
|
||||
property var draft: ({ position: "top", centerAnchor: "calendar", layout: { left: [], center: [], right: [] }, fontFamily: "JetBrainsMono Nerd Font" })
|
||||
property var registry: ({})
|
||||
property bool dirty: false
|
||||
property int draftRevision: 0
|
||||
|
||||
function cloneJson(value) {
|
||||
return JSON.parse(JSON.stringify(value || null))
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function mergeConfig(base, override) {
|
||||
var result = cloneJson(base || {})
|
||||
if (!isPlainObject(override)) return result
|
||||
for (var key in override) {
|
||||
if (isPlainObject(result[key]) && isPlainObject(override[key]))
|
||||
result[key] = mergeConfig(result[key], override[key])
|
||||
else
|
||||
result[key] = cloneJson(override[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function normalizeLayoutEntry(entry) {
|
||||
if (typeof entry === "string") return { id: entry }
|
||||
if (isPlainObject(entry) && entry.id) return cloneJson(entry)
|
||||
return null
|
||||
}
|
||||
|
||||
function normalizeLayout(layout) {
|
||||
var sections = ["left", "center", "right"]
|
||||
var result = {}
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var s = sections[i]
|
||||
var arr = []
|
||||
var src = (layout && layout[s]) || []
|
||||
for (var j = 0; j < src.length; j++) {
|
||||
var entry = normalizeLayoutEntry(src[j])
|
||||
if (entry) arr.push(entry)
|
||||
}
|
||||
result[s] = arr
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
var defaults = defaultsFile.text() ? JSON.parse(defaultsFile.text()) : {}
|
||||
defaultConfig = defaults
|
||||
} catch (e) {
|
||||
console.warn("Bad defaults JSON:", e)
|
||||
defaultConfig = {}
|
||||
}
|
||||
|
||||
var userText = userFile.text() || "{}"
|
||||
var user = {}
|
||||
try { user = JSON.parse(userText) } catch (e) { user = {} }
|
||||
|
||||
var merged = mergeConfig(defaultConfig, user)
|
||||
draft = {
|
||||
position: String(merged.position || "top"),
|
||||
centerAnchor: String(merged.centerAnchor || ""),
|
||||
fontFamily: String(merged.fontFamily || "JetBrainsMono Nerd Font"),
|
||||
layout: normalizeLayout(merged.layout || {})
|
||||
}
|
||||
dirty = false
|
||||
draftRevision++
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
var payload = cloneJson(draft)
|
||||
userFile.setText(JSON.stringify(payload, null, 2) + "\n")
|
||||
dirty = false
|
||||
}
|
||||
|
||||
function resetToDefaults() {
|
||||
userFile.setText("{}\n")
|
||||
loadConfig()
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
dirty = true
|
||||
draftRevision++
|
||||
}
|
||||
|
||||
function moveEntry(section, fromIndex, toIndex) {
|
||||
var arr = draft.layout[section].slice()
|
||||
if (toIndex < 0 || toIndex >= arr.length) return
|
||||
var item = arr[fromIndex]
|
||||
arr.splice(fromIndex, 1)
|
||||
arr.splice(toIndex, 0, item)
|
||||
draft.layout[section] = arr
|
||||
markDirty()
|
||||
}
|
||||
|
||||
function removeEntry(section, index) {
|
||||
var arr = draft.layout[section].slice()
|
||||
arr.splice(index, 1)
|
||||
draft.layout[section] = arr
|
||||
markDirty()
|
||||
}
|
||||
|
||||
function addEntry(section, id) {
|
||||
var arr = draft.layout[section].slice()
|
||||
arr.push({ id: id })
|
||||
draft.layout[section] = arr
|
||||
markDirty()
|
||||
}
|
||||
|
||||
function updateEntry(section, index, newEntry) {
|
||||
var arr = draft.layout[section].slice()
|
||||
arr[index] = cloneJson(newEntry)
|
||||
draft.layout[section] = arr
|
||||
markDirty()
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
// Catalog of available widgets — id → display metadata + settings schema path.
|
||||
// settingsForm is an inline-defined Component name that opens when editing.
|
||||
readonly property var catalog: ({
|
||||
"omarchy": { name: "Omarchy menu", description: "Launches the Omarchy menu" },
|
||||
"workspaces": { name: "Workspaces (legacy)", description: "Workspace numbers, no animation" },
|
||||
"workspacesPro": { name: "Workspaces", description: "Animated workspace switcher" },
|
||||
"activeWindow": { name: "Active window", description: "Title of the focused window" },
|
||||
"clock": { name: "Clock", description: "Date / time text" },
|
||||
"calendar": { name: "Calendar", description: "Clock with month-grid popup", settingsForm: "calendarSettings" },
|
||||
"media": { name: "Media", description: "MPRIS now-playing with controls" },
|
||||
"weather": { name: "Weather (legacy)", description: "Tiny weather pill" },
|
||||
"weatherFlyout": { name: "Weather", description: "Weather pill with detail popup" },
|
||||
"update": { name: "Updates", description: "Indicates available system updates" },
|
||||
"voxtype": { name: "Voxtype", description: "Voxtype dictation state" },
|
||||
"screenRecording": { name: "Screen recording", description: "Active recording indicator" },
|
||||
"idle": { name: "Idle (legacy)", description: "Inhibitor indicator" },
|
||||
"idleInhibitor": { name: "Keep awake", description: "Idle inhibitor toggle" },
|
||||
"notifications": { name: "DND (mako)", description: "Notification silencing indicator" },
|
||||
"notificationCenter": { name: "Notification center", description: "Recent notifications + DND (replaces mako)" },
|
||||
"tray": { name: "System tray", description: "Status notifier items" },
|
||||
"bluetooth": { name: "Bluetooth (legacy)", description: "Bluetooth status icon" },
|
||||
"bluetoothPanel": { name: "Bluetooth", description: "Bluetooth devices popup" },
|
||||
"network": { name: "Network (legacy)", description: "Wi-Fi/ethernet status" },
|
||||
"networkPanel": { name: "Network", description: "Wi-Fi list and connect" },
|
||||
"audio": { name: "Volume (legacy)", description: "Speaker icon, scroll for volume" },
|
||||
"audioPanel": { name: "Volume", description: "Volume slider, output picker, mixer" },
|
||||
"microphone": { name: "Microphone", description: "Mic input state" },
|
||||
"nightLight": { name: "Night light", description: "hyprsunset toggle" },
|
||||
"brightness": { name: "Brightness", description: "Screen brightness slider", settingsForm: "brightnessSettings" },
|
||||
"powerProfile": { name: "Power profile", description: "power-profiles-daemon selector" },
|
||||
"battery": { name: "Battery", description: "Battery percent and ETA" },
|
||||
"cpu": { name: "CPU (legacy)", description: "btop launcher" },
|
||||
"systemStats": { name: "System stats", description: "Inline CPU + RAM graphs" },
|
||||
"controlCenter": { name: "Quick settings", description: "Volume/brightness/DND/etc in one popup" },
|
||||
"powerMenu": { name: "Power menu", description: "Lock/suspend/reboot/shutdown" },
|
||||
"keyboardLayout": { name: "Keyboard layout", description: "Current xkb layout, click cycles" },
|
||||
"lockKeys": { name: "Lock keys", description: "Caps/Num/Scroll lock indicators" },
|
||||
"spacer": { name: "Spacer", description: "Configurable blank space", settingsForm: "spacerSettings" }
|
||||
})
|
||||
|
||||
function widgetName(id) {
|
||||
return catalog[id] ? catalog[id].name : id
|
||||
}
|
||||
|
||||
function widgetDescription(id) {
|
||||
return catalog[id] ? (catalog[id].description || "") : ""
|
||||
}
|
||||
|
||||
function widgetHasSettings(id) {
|
||||
return !!(catalog[id] && catalog[id].settingsForm)
|
||||
}
|
||||
|
||||
function availableToAdd(section) {
|
||||
var existingByOther = {}
|
||||
var sections = ["left", "center", "right"]
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
if (sections[s] === section) continue
|
||||
var list = draft.layout[sections[s]] || []
|
||||
for (var i = 0; i < list.length; i++) existingByOther[list[i].id] = true
|
||||
}
|
||||
var existingHere = {}
|
||||
var here = draft.layout[section] || []
|
||||
for (var j = 0; j < here.length; j++) existingHere[here[j].id] = true
|
||||
|
||||
var ids = Object.keys(catalog).sort(function(a, b) {
|
||||
return widgetName(a).localeCompare(widgetName(b))
|
||||
})
|
||||
|
||||
var result = []
|
||||
for (var k = 0; k < ids.length; k++) {
|
||||
var id = ids[k]
|
||||
// Allow multiple instances of `spacer` only.
|
||||
var allowMultiple = id === "spacer"
|
||||
if (!allowMultiple && existingHere[id]) continue
|
||||
result.push({ id: id, name: widgetName(id), description: widgetDescription(id), elsewhere: !!existingByOther[id] })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: defaultsFile
|
||||
path: root.defaultsPath
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onLoaded: root.loadConfig()
|
||||
onFileChanged: { reload(); root.loadConfig() }
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: userFile
|
||||
path: root.userConfigPath
|
||||
watchChanges: true
|
||||
atomicWrites: true
|
||||
printErrors: false
|
||||
onLoaded: root.loadConfig()
|
||||
onFileChanged: { reload(); root.loadConfig() }
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: root.home + "/.config/omarchy/current/theme/colors.toml"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onLoaded: root.loadTheme(text())
|
||||
onFileChanged: { reload(); root.loadTheme(text()) }
|
||||
}
|
||||
|
||||
FloatingWindow {
|
||||
id: window
|
||||
title: "Omarchy bar settings"
|
||||
color: root.background
|
||||
implicitWidth: 720
|
||||
implicitHeight: 720
|
||||
minimumSize: Qt.size(560, 500)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.background
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
spacing: 16
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 32
|
||||
|
||||
Text {
|
||||
text: "Bar settings"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 8
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
ActionPill {
|
||||
text: "Reset to defaults"
|
||||
foreground: root.urgent
|
||||
onClicked: root.resetToDefaults()
|
||||
}
|
||||
|
||||
ActionPill {
|
||||
text: root.dirty ? "Save" : "Saved"
|
||||
foreground: root.dirty ? root.accent : Qt.darker(root.foreground, 1.5)
|
||||
bordered: root.dirty
|
||||
onClicked: if (root.dirty) root.saveConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
OptionDropdown {
|
||||
label: "Position"
|
||||
value: root.draft.position
|
||||
options: ["top", "right", "bottom", "left"]
|
||||
onChanged: function(v) {
|
||||
root.draft.position = v
|
||||
root.markDirty()
|
||||
}
|
||||
}
|
||||
|
||||
OptionDropdown {
|
||||
label: "Center anchor"
|
||||
value: root.draft.centerAnchor
|
||||
options: {
|
||||
var list = ["(none)"]
|
||||
var entries = root.draft.layout.center || []
|
||||
for (var i = 0; i < entries.length; i++) list.push(entries[i].id)
|
||||
return list
|
||||
}
|
||||
onChanged: function(v) {
|
||||
root.draft.centerAnchor = v === "(none)" ? "" : v
|
||||
root.markDirty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: bodyScroll
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: bodyColumn.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
ColumnLayout {
|
||||
id: bodyColumn
|
||||
width: bodyScroll.width
|
||||
spacing: 14
|
||||
|
||||
SectionEditor { sectionKey: "left"; sectionLabel: "Left" }
|
||||
SectionEditor { sectionKey: "center"; sectionLabel: "Center" }
|
||||
SectionEditor { sectionKey: "right"; sectionLabel: "Right" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Components ---------------------------------------------------
|
||||
|
||||
component ActionPill: Rectangle {
|
||||
id: pill
|
||||
property string text: ""
|
||||
property color foreground: root.foreground
|
||||
property bool bordered: true
|
||||
signal clicked()
|
||||
|
||||
implicitWidth: pillLabel.implicitWidth + 22
|
||||
implicitHeight: 26
|
||||
radius: 4
|
||||
color: pillArea.containsMouse ? Qt.rgba(pill.foreground.r, pill.foreground.g, pill.foreground.b, 0.15) : "transparent"
|
||||
border.color: pill.bordered ? pill.foreground : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
id: pillLabel
|
||||
anchors.centerIn: parent
|
||||
text: pill.text
|
||||
color: pill.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pillArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: pill.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
component OptionDropdown: Item {
|
||||
id: dropdown
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property var options: []
|
||||
signal changed(string value)
|
||||
|
||||
implicitWidth: 240
|
||||
implicitHeight: 48
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: dropdown.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: combo
|
||||
width: parent.width
|
||||
height: 28
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
model: dropdown.options
|
||||
currentIndex: {
|
||||
for (var i = 0; i < model.length; i++) if (model[i] === dropdown.value) return i
|
||||
return 0
|
||||
}
|
||||
|
||||
onActivated: function(index) {
|
||||
dropdown.changed(model[index])
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: root.background
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
|
||||
border.width: 1
|
||||
radius: 4
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
leftPadding: 8
|
||||
rightPadding: 24
|
||||
text: combo.displayText
|
||||
color: root.foreground
|
||||
font: combo.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SectionEditor: Column {
|
||||
id: section
|
||||
|
||||
property string sectionKey: ""
|
||||
property string sectionLabel: ""
|
||||
property var entries: (root.draft.layout && root.draft.layout[section.sectionKey]) || []
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDraftRevisionChanged() { section.entries = (root.draft.layout && root.draft.layout[section.sectionKey]) || [] }
|
||||
}
|
||||
|
||||
Row {
|
||||
width: section.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: section.sectionLabel
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "· " + section.entries.length + (section.entries.length === 1 ? " widget" : " widgets")
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: section.width - 200 - parent.children[0].implicitWidth - parent.children[1].implicitWidth; height: 1 }
|
||||
|
||||
ActionPill {
|
||||
text: "+ Add widget"
|
||||
onClicked: addMenu.popup()
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: addMenu
|
||||
Repeater {
|
||||
model: root.availableToAdd(section.sectionKey)
|
||||
delegate: MenuItem {
|
||||
required property var modelData
|
||||
text: modelData.name + (modelData.elsewhere ? " (elsewhere)" : "")
|
||||
onTriggered: root.addEntry(section.sectionKey, modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
width: section.width
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: section.entries
|
||||
delegate: WidgetCard {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: section.width
|
||||
sectionKey: section.sectionKey
|
||||
entryIndex: index
|
||||
entry: modelData
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: section.entries.length === 0
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: 4
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Empty — add a widget"
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WidgetCard: Rectangle {
|
||||
id: card
|
||||
property string sectionKey: ""
|
||||
property int entryIndex: -1
|
||||
property var entry: ({})
|
||||
readonly property string entryId: entry && entry.id ? String(entry.id) : ""
|
||||
readonly property string displayName: root.widgetName(entryId)
|
||||
readonly property string description: root.widgetDescription(entryId)
|
||||
readonly property bool hasSettings: root.widgetHasSettings(entryId)
|
||||
|
||||
implicitHeight: 50
|
||||
radius: 4
|
||||
color: cardArea.containsMouse ? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.08) : Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.03)
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Row {
|
||||
id: actionRow
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
IconButton {
|
||||
glyph: "↑"
|
||||
tooltip: "Move up"
|
||||
onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex - 1)
|
||||
}
|
||||
IconButton {
|
||||
glyph: "↓"
|
||||
tooltip: "Move down"
|
||||
onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex + 1)
|
||||
}
|
||||
IconButton {
|
||||
glyph: "⚙"
|
||||
tooltip: "Settings"
|
||||
visible: card.hasSettings
|
||||
onClicked: settingsLoader.open(card.entry)
|
||||
}
|
||||
IconButton {
|
||||
glyph: "✕"
|
||||
tooltip: "Remove"
|
||||
foreground: root.urgent
|
||||
onClicked: root.removeEntry(card.sectionKey, card.entryIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: actionRow.left
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: card.displayName
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: card.description
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cardArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
SettingsDialog {
|
||||
id: settingsLoader
|
||||
anchorWindow: window
|
||||
sectionKey: card.sectionKey
|
||||
entryIndex: card.entryIndex
|
||||
}
|
||||
}
|
||||
|
||||
component IconButton: Rectangle {
|
||||
id: iconButton
|
||||
property string glyph: ""
|
||||
property string tooltip: ""
|
||||
property color foreground: root.foreground
|
||||
signal clicked()
|
||||
|
||||
implicitWidth: 26
|
||||
implicitHeight: 26
|
||||
radius: 3
|
||||
color: iconArea.containsMouse ? Qt.rgba(iconButton.foreground.r, iconButton.foreground.g, iconButton.foreground.b, 0.18) : "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: iconButton.glyph
|
||||
color: iconButton.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: iconArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: iconButton.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
component SettingsDialog: Item {
|
||||
id: dialog
|
||||
property var anchorWindow: null
|
||||
property string sectionKey: ""
|
||||
property int entryIndex: -1
|
||||
property var workingEntry: ({})
|
||||
|
||||
function open(entry) {
|
||||
workingEntry = root.cloneJson(entry)
|
||||
win.visible = true
|
||||
}
|
||||
|
||||
function commit() {
|
||||
root.updateEntry(sectionKey, entryIndex, workingEntry)
|
||||
win.visible = false
|
||||
}
|
||||
|
||||
function discard() {
|
||||
win.visible = false
|
||||
}
|
||||
|
||||
function fieldChanged(key, value) {
|
||||
var copy = root.cloneJson(workingEntry)
|
||||
copy[key] = value
|
||||
workingEntry = copy
|
||||
}
|
||||
|
||||
FloatingWindow {
|
||||
id: win
|
||||
title: "Widget settings — " + root.widgetName(dialog.workingEntry.id || "")
|
||||
color: root.background
|
||||
implicitWidth: 380
|
||||
implicitHeight: 320
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.background
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 18
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: root.widgetName(dialog.workingEntry.id || "")
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.widgetDescription(dialog.workingEntry.id || "")
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: formLoader
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: formComponent(dialog.workingEntry.id || "")
|
||||
onLoaded: {
|
||||
if (item && "entry" in item) item.entry = dialog.workingEntry
|
||||
if (item && "fieldChanged" in item) {
|
||||
item.fieldChanged.connect(function(key, value) { dialog.fieldChanged(key, value) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
Row {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: 8
|
||||
ActionPill { text: "Cancel"; bordered: false; onClicked: dialog.discard() }
|
||||
ActionPill { text: "Apply"; onClicked: dialog.commit() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formComponent(id) {
|
||||
var cat = catalog[id]
|
||||
if (!cat || !cat.settingsForm) return null
|
||||
switch (cat.settingsForm) {
|
||||
case "spacerSettings": return spacerSettingsComponent
|
||||
case "calendarSettings": return calendarSettingsComponent
|
||||
case "brightnessSettings": return brightnessSettingsComponent
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: spacerSettingsComponent
|
||||
|
||||
Column {
|
||||
id: spacerForm
|
||||
signal fieldChanged(string key, var value)
|
||||
property var entry: ({})
|
||||
|
||||
spacing: 8
|
||||
width: parent ? parent.width : 0
|
||||
|
||||
Text {
|
||||
text: "Size (pixels)"
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
from: 0
|
||||
to: 256
|
||||
value: spacerForm.entry.size !== undefined ? spacerForm.entry.size : 12
|
||||
onValueChanged: spacerForm.fieldChanged("size", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: calendarSettingsComponent
|
||||
|
||||
Column {
|
||||
id: calForm
|
||||
signal fieldChanged(string key, var value)
|
||||
property var entry: ({})
|
||||
|
||||
spacing: 8
|
||||
width: parent ? parent.width : 0
|
||||
|
||||
Text {
|
||||
text: "Horizontal format"
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
TextField {
|
||||
text: calForm.entry.format || "dddd HH:mm"
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
width: parent.width
|
||||
onEditingFinished: calForm.fieldChanged("format", text)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Alternate format (click to swap)"
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
TextField {
|
||||
text: calForm.entry.formatAlt || "dd MMMM 'W'ww yyyy"
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
width: parent.width
|
||||
onEditingFinished: calForm.fieldChanged("formatAlt", text)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Vertical format (left/right bars)"
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
TextField {
|
||||
text: calForm.entry.verticalFormat || "HH\n—\nmm"
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
width: parent.width
|
||||
onEditingFinished: calForm.fieldChanged("verticalFormat", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: brightnessSettingsComponent
|
||||
|
||||
Column {
|
||||
id: brightForm
|
||||
signal fieldChanged(string key, var value)
|
||||
property var entry: ({})
|
||||
|
||||
spacing: 8
|
||||
width: parent ? parent.width : 0
|
||||
|
||||
Text {
|
||||
text: "Scroll step (% per notch)"
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
SpinBox {
|
||||
from: 1
|
||||
to: 25
|
||||
value: brightForm.entry.step !== undefined ? brightForm.entry.step : 5
|
||||
onValueChanged: brightForm.fieldChanged("step", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,21 +9,40 @@ This is the Quickshell implementation of the Omarchy status bar.
|
||||
- User overrides live in `~/.config/omarchy/bar.json` and are merged over defaults at runtime.
|
||||
- `omarchy-style-bar-position` updates only the user override file.
|
||||
|
||||
Example user override:
|
||||
## Customizing
|
||||
|
||||
The bar reads `~/.local/share/omarchy/default/quickshell/bar/bar-defaults.json`, then deep-merges `~/.config/omarchy/bar.json` on top of it. Each `layout.{left,center,right}` entry is an object: at minimum `{ "id": "<widget>" }`, plus any inline settings the widget reads.
|
||||
|
||||
Launch the visual editor with `omarchy launch bar-settings` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand.
|
||||
|
||||
Example `bar.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"position": "top",
|
||||
"centerAnchor": "calendar",
|
||||
"layout": {
|
||||
"left": ["omarchy", "workspacesPro"],
|
||||
"center": ["media", "calendar", "weatherFlyout"],
|
||||
"right": ["systemStats", "notificationCenter", "bluetoothPanel", "networkPanel", "audioPanel", "brightness", "powerProfile", "battery", "powerMenu"]
|
||||
},
|
||||
"centerAnchor": "calendar"
|
||||
"left": [
|
||||
{ "id": "omarchy" },
|
||||
{ "id": "spacer", "size": 12 },
|
||||
{ "id": "workspacesPro" }
|
||||
],
|
||||
"center": [
|
||||
{ "id": "media" },
|
||||
{ "id": "calendar", "format": "HH:mm" }
|
||||
],
|
||||
"right": [
|
||||
{ "id": "systemStats" },
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "battery" },
|
||||
{ "id": "controlCenter" },
|
||||
{ "id": "powerMenu" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`centerAnchor` pins one center module to the exact horizontal/vertical center and flanks others around it.
|
||||
`centerAnchor` pins one center module to the exact horizontal/vertical center and flanks others around it. Set to an empty string to disable anchoring (the center list is centered as a group).
|
||||
|
||||
## Module catalogue
|
||||
|
||||
@@ -58,23 +77,18 @@ All widgets work in `top`, `bottom`, `left`, and `right` positions. Popups ancho
|
||||
|
||||
## Custom user modules
|
||||
|
||||
Add a module name to a layout list, then define it under `modules` in `~/.config/omarchy/bar.json`.
|
||||
The schema accepts arbitrary module ids that you provide. Set `type` to `command` for shell-driven output or `qml` for a custom QML widget.
|
||||
|
||||
For simple text/JSON output, use a command module:
|
||||
Command module:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": {
|
||||
"right": ["tray", "vpn", "audioPanel", "cpu"]
|
||||
},
|
||||
"modules": {
|
||||
"vpn": {
|
||||
"type": "command",
|
||||
"exec": "~/.config/omarchy/bar/scripts/vpn-status",
|
||||
"interval": 5,
|
||||
"tooltip": "VPN",
|
||||
"onClick": "nm-connection-editor"
|
||||
}
|
||||
"right": [
|
||||
{ "id": "tray" },
|
||||
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
|
||||
{ "id": "audioPanel" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -85,17 +99,15 @@ The command may print plain text or Waybar-style JSON, for example:
|
||||
{"text":"","tooltip":"Work VPN","class":"active"}
|
||||
```
|
||||
|
||||
For advanced custom UI, use QML:
|
||||
QML module:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": {
|
||||
"right": ["gpu", "audioPanel", "cpu"]
|
||||
},
|
||||
"modules": {
|
||||
"gpu": {
|
||||
"type": "qml"
|
||||
}
|
||||
"right": [
|
||||
{ "id": "gpu", "type": "qml" },
|
||||
{ "id": "audioPanel" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,20 +3,34 @@
|
||||
"fontFamily": "JetBrainsMono Nerd Font",
|
||||
"centerAnchor": "calendar",
|
||||
"layout": {
|
||||
"left": ["omarchy", "workspacesPro", "activeWindow"],
|
||||
"center": ["media", "calendar", "weatherFlyout", "update", "voxtype", "screenRecording", "idle", "notifications"],
|
||||
"right": ["tray", "systemStats", "microphone", "bluetoothPanel", "networkPanel", "audioPanel", "nightLight", "brightness", "powerProfile", "battery", "controlCenter", "powerMenu"]
|
||||
},
|
||||
"modules": {
|
||||
"calendar": {
|
||||
"format": "dddd HH:mm",
|
||||
"formatAlt": "dd MMMM 'W'ww yyyy",
|
||||
"verticalFormat": "HH\n—\nmm"
|
||||
},
|
||||
"clock": {
|
||||
"format": "dddd HH:mm",
|
||||
"formatAlt": "dd MMMM 'W'ww yyyy",
|
||||
"verticalFormat": "HH\n—\nmm"
|
||||
}
|
||||
"left": [
|
||||
{ "id": "omarchy" },
|
||||
{ "id": "workspacesPro" },
|
||||
{ "id": "activeWindow" }
|
||||
],
|
||||
"center": [
|
||||
{ "id": "media" },
|
||||
{ "id": "calendar", "format": "dddd HH:mm", "formatAlt": "dd MMMM 'W'ww yyyy", "verticalFormat": "HH\n—\nmm" },
|
||||
{ "id": "weatherFlyout" },
|
||||
{ "id": "update" },
|
||||
{ "id": "voxtype" },
|
||||
{ "id": "screenRecording" },
|
||||
{ "id": "idle" },
|
||||
{ "id": "notifications" }
|
||||
],
|
||||
"right": [
|
||||
{ "id": "tray" },
|
||||
{ "id": "systemStats" },
|
||||
{ "id": "microphone" },
|
||||
{ "id": "bluetoothPanel" },
|
||||
{ "id": "networkPanel" },
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "nightLight" },
|
||||
{ "id": "brightness" },
|
||||
{ "id": "powerProfile" },
|
||||
{ "id": "battery" },
|
||||
{ "id": "controlCenter" },
|
||||
{ "id": "powerMenu" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,22 +21,21 @@ ShellRoot {
|
||||
fontFamily: "JetBrainsMono Nerd Font",
|
||||
centerAnchor: "clock",
|
||||
layout: {
|
||||
left: ["omarchy", "workspaces"],
|
||||
center: ["clock", "weather", "update", "voxtype", "screenRecording", "idle", "notifications"],
|
||||
right: ["tray", "bluetooth", "network", "audio", "cpu", "battery"]
|
||||
},
|
||||
modules: {
|
||||
clock: {
|
||||
format: "dddd HH:mm",
|
||||
formatAlt: "dd MMMM 'W'ww yyyy",
|
||||
verticalFormat: "HH\n—\nmm"
|
||||
}
|
||||
left: [{ id: "omarchy" }, { id: "workspaces" }],
|
||||
center: [
|
||||
{ id: "clock", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n—\nmm" },
|
||||
{ id: "weather" }, { id: "update" }, { id: "voxtype" },
|
||||
{ id: "screenRecording" }, { id: "idle" }, { id: "notifications" }
|
||||
],
|
||||
right: [
|
||||
{ id: "tray" }, { id: "bluetooth" }, { id: "network" },
|
||||
{ id: "audio" }, { id: "cpu" }, { id: "battery" }
|
||||
]
|
||||
}
|
||||
})
|
||||
property var defaultBarConfig: builtinBarConfig
|
||||
property var userBarConfig: ({})
|
||||
property var layoutConfig: builtinBarConfig.layout
|
||||
property var moduleConfig: builtinBarConfig.modules
|
||||
property string centerAnchor: "clock"
|
||||
property int barConfigSerial: 0
|
||||
property string position: "top"
|
||||
@@ -145,14 +144,38 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLayoutEntry(entry) {
|
||||
if (typeof entry === "string") return { id: entry }
|
||||
if (isPlainObject(entry) && entry.id) return entry
|
||||
return null
|
||||
}
|
||||
|
||||
function normalizeLayoutSection(list) {
|
||||
if (!Array.isArray(list)) return []
|
||||
var result = []
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var normalized = normalizeLayoutEntry(list[i])
|
||||
if (normalized) result.push(normalized)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function normalizeLayout(layout) {
|
||||
if (!isPlainObject(layout)) layout = builtinBarConfig.layout
|
||||
return {
|
||||
left: normalizeLayoutSection(layout.left),
|
||||
center: normalizeLayoutSection(layout.center),
|
||||
right: normalizeLayoutSection(layout.right)
|
||||
}
|
||||
}
|
||||
|
||||
function applyBarConfig() {
|
||||
var config = mergeConfig(defaultBarConfig, userBarConfig)
|
||||
|
||||
position = normalizePosition(config.position)
|
||||
fontFamily = String(config.fontFamily || "JetBrainsMono Nerd Font")
|
||||
centerAnchor = String(config.centerAnchor || "clock")
|
||||
layoutConfig = isPlainObject(config.layout) ? config.layout : builtinBarConfig.layout
|
||||
moduleConfig = isPlainObject(config.modules) ? config.modules : {}
|
||||
layoutConfig = normalizeLayout(config.layout)
|
||||
barConfigSerial++
|
||||
}
|
||||
|
||||
@@ -166,42 +189,53 @@ ShellRoot {
|
||||
applyBarConfig()
|
||||
}
|
||||
|
||||
function layoutModules(region) {
|
||||
function layoutEntries(region) {
|
||||
var serial = barConfigSerial
|
||||
var modules = layoutConfig ? layoutConfig[region] : null
|
||||
return Array.isArray(modules) ? modules : []
|
||||
var entries = layoutConfig ? layoutConfig[region] : null
|
||||
return Array.isArray(entries) ? entries : []
|
||||
}
|
||||
|
||||
function moduleSettings(name) {
|
||||
var serial = barConfigSerial
|
||||
var settings = moduleConfig ? moduleConfig[String(name)] : null
|
||||
return isPlainObject(settings) ? settings : {}
|
||||
function entrySettings(entry) {
|
||||
if (!isPlainObject(entry)) return {}
|
||||
var copy = {}
|
||||
for (var key in entry) {
|
||||
if (key === "id") continue
|
||||
copy[key] = entry[key]
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
function moduleString(name, key, fallback) {
|
||||
var value = moduleSettings(name)[key]
|
||||
function entryId(entry) {
|
||||
if (typeof entry === "string") return entry
|
||||
if (isPlainObject(entry) && entry.id) return String(entry.id)
|
||||
return ""
|
||||
}
|
||||
|
||||
function moduleString(entry, key, fallback) {
|
||||
var settings = entrySettings(entry)
|
||||
var value = settings[key]
|
||||
return value === undefined || value === null ? fallback : String(value)
|
||||
}
|
||||
|
||||
function moduleIndex(modules, name) {
|
||||
if (!Array.isArray(modules)) return -1
|
||||
function entryIndex(entries, name) {
|
||||
if (!Array.isArray(entries)) return -1
|
||||
|
||||
for (var i = 0; i < modules.length; i++) {
|
||||
if (String(modules[i]) === name)
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
if (entryId(entries[i]) === name)
|
||||
return i
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
function modulesBefore(modules, name) {
|
||||
var index = moduleIndex(modules, name)
|
||||
return index <= 0 ? [] : modules.slice(0, index)
|
||||
function entriesBefore(entries, name) {
|
||||
var index = entryIndex(entries, name)
|
||||
return index <= 0 ? [] : entries.slice(0, index)
|
||||
}
|
||||
|
||||
function modulesAfter(modules, name) {
|
||||
var index = moduleIndex(modules, name)
|
||||
return index === -1 ? [] : modules.slice(index + 1)
|
||||
function entriesAfter(entries, name) {
|
||||
var index = entryIndex(entries, name)
|
||||
return index === -1 ? [] : entries.slice(index + 1)
|
||||
}
|
||||
|
||||
function builtinModuleComponent(name) {
|
||||
@@ -241,8 +275,8 @@ ShellRoot {
|
||||
return value !== "" && value.indexOf("..") === -1 && value[0] !== "/"
|
||||
}
|
||||
|
||||
function customModuleType(name) {
|
||||
var settings = moduleSettings(name)
|
||||
function customModuleType(entry) {
|
||||
var settings = entrySettings(entry)
|
||||
var type = String(settings.type || "")
|
||||
if (type) return type
|
||||
if (settings.exec) return "command"
|
||||
@@ -250,8 +284,9 @@ ShellRoot {
|
||||
return ""
|
||||
}
|
||||
|
||||
function customModuleSource(name) {
|
||||
var settings = moduleSettings(name)
|
||||
function customModuleSource(entry) {
|
||||
var settings = entrySettings(entry)
|
||||
var name = entryId(entry)
|
||||
var source = settings.source ? expandPath(settings.source) : ""
|
||||
if (!source && customModuleSafeName(name))
|
||||
source = omarchyConfigDir + "/bar/modules/" + String(name) + ".qml"
|
||||
@@ -601,14 +636,26 @@ ShellRoot {
|
||||
root.run("hyprctl dispatch " + shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })"))
|
||||
}
|
||||
|
||||
function clockEntry() {
|
||||
var serial = barConfigSerial
|
||||
var sections = ["left", "center", "right"]
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var entries = layoutEntries(sections[i])
|
||||
var idx = entryIndex(entries, "clock")
|
||||
if (idx !== -1) return entries[idx]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function clockText() {
|
||||
var entry = clockEntry()
|
||||
if (clockAlt)
|
||||
return Qt.formatDateTime(systemClock.date, moduleString("clock", "formatAlt", "dd MMMM 'W'ww yyyy"))
|
||||
return Qt.formatDateTime(systemClock.date, moduleString(entry, "formatAlt", "dd MMMM 'W'ww yyyy"))
|
||||
|
||||
if (vertical)
|
||||
return Qt.formatDateTime(systemClock.date, moduleString("clock", "verticalFormat", "HH\n—\nmm"))
|
||||
return Qt.formatDateTime(systemClock.date, moduleString(entry, "verticalFormat", "HH\n—\nmm"))
|
||||
|
||||
return Qt.formatDateTime(systemClock.date, moduleString("clock", "format", "dddd HH:mm"))
|
||||
return Qt.formatDateTime(systemClock.date, moduleString(entry, "format", "dddd HH:mm"))
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
@@ -947,19 +994,26 @@ ShellRoot {
|
||||
Component { id: cpuModuleComponent; CpuModule {} }
|
||||
Component { id: batteryModuleComponent; BatteryModule {} }
|
||||
|
||||
function findCenterAnchorEntry() {
|
||||
var entries = root.layoutEntries("center")
|
||||
var idx = root.entryIndex(entries, root.centerAnchor)
|
||||
return idx === -1 ? null : entries[idx]
|
||||
}
|
||||
|
||||
component LeftModules: ModuleList {
|
||||
modules: root.layoutModules("left")
|
||||
entries: root.layoutEntries("left")
|
||||
}
|
||||
|
||||
component RightModules: ModuleList {
|
||||
modules: root.layoutModules("right")
|
||||
entries: root.layoutEntries("right")
|
||||
}
|
||||
|
||||
component CenterModules: Item {
|
||||
id: centerRoot
|
||||
|
||||
property var modules: root.layoutModules("center")
|
||||
readonly property bool hasAnchor: root.moduleIndex(modules, root.centerAnchor) !== -1
|
||||
property var entries: root.layoutEntries("center")
|
||||
readonly property bool hasAnchor: root.entryIndex(entries, root.centerAnchor) !== -1
|
||||
readonly property var anchorEntry: root.findCenterAnchorEntry()
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
@@ -974,13 +1028,13 @@ ShellRoot {
|
||||
|
||||
ModuleList {
|
||||
visible: !centerRoot.hasAnchor
|
||||
modules: centerRoot.modules
|
||||
entries: centerRoot.entries
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
ModuleList {
|
||||
visible: centerRoot.hasAnchor
|
||||
modules: root.modulesBefore(centerRoot.modules, root.centerAnchor)
|
||||
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
||||
anchors.right: centerAnchorModule.left
|
||||
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
||||
}
|
||||
@@ -988,13 +1042,13 @@ ShellRoot {
|
||||
ModuleSlot {
|
||||
id: centerAnchorModule
|
||||
visible: centerRoot.hasAnchor
|
||||
moduleName: root.centerAnchor
|
||||
entry: centerRoot.anchorEntry
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
ModuleList {
|
||||
visible: centerRoot.hasAnchor
|
||||
modules: root.modulesAfter(centerRoot.modules, root.centerAnchor)
|
||||
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
||||
anchors.left: centerAnchorModule.right
|
||||
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
||||
}
|
||||
@@ -1009,13 +1063,13 @@ ShellRoot {
|
||||
|
||||
ModuleList {
|
||||
visible: !centerRoot.hasAnchor
|
||||
modules: centerRoot.modules
|
||||
entries: centerRoot.entries
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
ModuleList {
|
||||
visible: centerRoot.hasAnchor
|
||||
modules: root.modulesBefore(centerRoot.modules, root.centerAnchor)
|
||||
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
||||
anchors.bottom: centerAnchorModule.top
|
||||
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
||||
}
|
||||
@@ -1023,13 +1077,13 @@ ShellRoot {
|
||||
ModuleSlot {
|
||||
id: centerAnchorModule
|
||||
visible: centerRoot.hasAnchor
|
||||
moduleName: root.centerAnchor
|
||||
entry: centerRoot.anchorEntry
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
ModuleList {
|
||||
visible: centerRoot.hasAnchor
|
||||
modules: root.modulesAfter(centerRoot.modules, root.centerAnchor)
|
||||
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
||||
anchors.top: centerAnchorModule.bottom
|
||||
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
||||
}
|
||||
@@ -1040,9 +1094,9 @@ ShellRoot {
|
||||
component ModuleList: Loader {
|
||||
id: moduleListRoot
|
||||
|
||||
property var modules: []
|
||||
property var entries: []
|
||||
|
||||
visible: modules.length > 0
|
||||
visible: entries.length > 0
|
||||
sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList
|
||||
width: item ? item.implicitWidth : 0
|
||||
height: item ? item.implicitHeight : 0
|
||||
@@ -1054,11 +1108,11 @@ ShellRoot {
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: moduleListRoot.modules
|
||||
model: moduleListRoot.entries
|
||||
|
||||
ModuleSlot {
|
||||
required property var modelData
|
||||
moduleName: String(modelData)
|
||||
entry: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1071,11 +1125,11 @@ ShellRoot {
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: moduleListRoot.modules
|
||||
model: moduleListRoot.entries
|
||||
|
||||
ModuleSlot {
|
||||
required property var modelData
|
||||
moduleName: String(modelData)
|
||||
entry: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1085,8 +1139,10 @@ ShellRoot {
|
||||
component ModuleSlot: Item {
|
||||
id: slot
|
||||
|
||||
required property string moduleName
|
||||
readonly property string customType: root.customModuleType(moduleName)
|
||||
required property var entry
|
||||
readonly property string moduleName: root.entryId(entry)
|
||||
readonly property var moduleSettings: root.entrySettings(entry)
|
||||
readonly property string customType: root.customModuleType(entry)
|
||||
readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName)
|
||||
readonly property string firstPartySource: customType || builtinComponent ? "" : root.firstPartyWidgetSource(moduleName)
|
||||
readonly property bool qmlCustom: customType === "qml"
|
||||
@@ -1109,35 +1165,33 @@ ShellRoot {
|
||||
Loader {
|
||||
id: qmlLoader
|
||||
active: slot.qmlCustom || slot.firstParty
|
||||
source: slot.qmlCustom ? root.customModuleSource(slot.moduleName) : (slot.firstParty ? slot.firstPartySource : "")
|
||||
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : (slot.firstParty ? slot.firstPartySource : "")
|
||||
anchors.fill: parent
|
||||
onLoaded: slot.injectProps()
|
||||
}
|
||||
|
||||
onModuleSettingsChanged: injectProps()
|
||||
|
||||
function injectProps() {
|
||||
var target = qmlLoader.item
|
||||
if (!target) return
|
||||
if ("bar" in target) target.bar = root
|
||||
if ("moduleName" in target) target.moduleName = moduleName
|
||||
if ("settings" in target) target.settings = root.moduleSettings(moduleName)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onBarConfigSerialChanged() { slot.injectProps() }
|
||||
if ("settings" in target) target.settings = moduleSettings
|
||||
}
|
||||
|
||||
Component {
|
||||
id: customCommandModuleComponent
|
||||
CustomCommandModule { moduleName: slot.moduleName }
|
||||
CustomCommandModule { entry: slot.entry }
|
||||
}
|
||||
}
|
||||
|
||||
component CustomCommandModule: ModuleButton {
|
||||
id: customRoot
|
||||
|
||||
required property string moduleName
|
||||
property var settings: root.moduleSettings(moduleName)
|
||||
required property var entry
|
||||
readonly property string moduleName: root.entryId(entry)
|
||||
readonly property var settings: root.entrySettings(entry)
|
||||
property string outputText: ""
|
||||
property string outputTooltip: ""
|
||||
property bool outputActive: false
|
||||
|
||||
@@ -322,6 +322,16 @@ Item {
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Customize bar…"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: { root.run("omarchy-launch-bar-settings"); root.popupOpen = false }
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Reference in New Issue
Block a user