Move bar-settings and background-switcher into the shell
This commit is contained in:
@@ -5,14 +5,4 @@
|
||||
# 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
|
||||
if ! hyprctl dispatch focuswindow 'title:Omarchy bar settings' 2>/dev/null | grep -q ok; then
|
||||
hyprctl dispatch focuswindow 'initialTitle:Omarchy bar settings' >/dev/null 2>&1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$CONFIG_DIR" >/dev/null 2>&1 &
|
||||
exec omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
|
||||
|
||||
@@ -84,7 +84,7 @@ done_file=$(mktemp)
|
||||
rm -f "$done_file"
|
||||
trap 'rm -f "$selection_file" "$done_file"' EXIT
|
||||
socket_path="${XDG_RUNTIME_DIR:-/run/user/$UID}/omarchy-image-selector.sock"
|
||||
selector_qml="$OMARCHY_PATH/default/quickshell/background-switcher.qml"
|
||||
shell_dir="$OMARCHY_PATH/default/quickshell/omarchy-shell"
|
||||
|
||||
image_dirs_env=""
|
||||
for dir in "${image_dirs[@]}"; do
|
||||
@@ -230,14 +230,11 @@ if [[ $cache_only == true || $prepare_only == true ]]; then
|
||||
fi
|
||||
|
||||
ensure_selector() {
|
||||
if [[ -S $socket_path && $selector_qml -nt $socket_path ]]; then
|
||||
quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true
|
||||
rm -f "$socket_path"
|
||||
fi
|
||||
|
||||
# The image-selector socket is owned by omarchy-shell. Make sure the shell is
|
||||
# alive; once it is, the BackgroundSwitcher plugin keeps the socket bound for
|
||||
# the lifetime of the shell.
|
||||
if [[ ! -S $socket_path ]]; then
|
||||
quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true
|
||||
quickshell -d -p "$selector_qml" >/dev/null
|
||||
omarchy-shell-ipc shell ping >/dev/null 2>&1 || true
|
||||
|
||||
for ((i = 0; i < 100; i++)); do
|
||||
if [[ -S $socket_path ]]; then
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Send an IPC call to omarchy-shell, starting it if not running
|
||||
# omarchy:hidden=true
|
||||
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}"
|
||||
SHELL_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell"
|
||||
|
||||
if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then
|
||||
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$SHELL_DIR" >/dev/null 2>&1 &
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||
sleep 0.2
|
||||
quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance ' && break
|
||||
done
|
||||
fi
|
||||
|
||||
exec quickshell ipc -p "$SHELL_DIR" call "$@"
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.background-switcher",
|
||||
"name": "Background switcher",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Wallpaper / image picker overlay",
|
||||
"kinds": ["overlay"],
|
||||
"activation": "on-demand",
|
||||
"keepLoaded": true,
|
||||
"entryPoints": { "overlay": "BackgroundSwitcher.qml" },
|
||||
"ipc": {
|
||||
"summon": "background-switcher",
|
||||
"legacySocket": "omarchy-image-selector.sock"
|
||||
}
|
||||
}
|
||||
+405
-67
@@ -4,19 +4,28 @@ import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
ShellRoot {
|
||||
import "../../ui/settings" as SettingsUi
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string home: Quickshell.env("HOME")
|
||||
function deriveOmarchyPath() {
|
||||
// Injected by the host shell when the panel is summoned. Shared instances
|
||||
// so the panel sees the same registry state the bar wrote into.
|
||||
property var barWidgetRegistry: null
|
||||
property var pluginRegistry: null
|
||||
|
||||
// Not `required` so the Loader-based instantiation can satisfy it via
|
||||
// onLoaded; we still gracefully fall back to deriving from shellDir for
|
||||
// standalone QML tooling.
|
||||
property string omarchyPath: {
|
||||
var env = Quickshell.env("OMARCHY_PATH")
|
||||
if (env) return env
|
||||
var dir = String(Quickshell.shellDir || "")
|
||||
if (dir.indexOf("/default/quickshell/bar-settings") !== -1)
|
||||
return dir.substring(0, dir.indexOf("/default/quickshell/bar-settings"))
|
||||
return home + "/.local/share/omarchy"
|
||||
if (dir.indexOf("/default/quickshell/omarchy-shell") !== -1)
|
||||
return dir.substring(0, dir.indexOf("/default/quickshell/omarchy-shell"))
|
||||
return Quickshell.env("HOME") + "/.local/share/omarchy"
|
||||
}
|
||||
property string omarchyPath: deriveOmarchyPath()
|
||||
readonly property string home: Quickshell.env("HOME")
|
||||
readonly property string userConfigPath: home + "/.config/omarchy/bar.json"
|
||||
readonly property string defaultsPath: omarchyPath + "/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json"
|
||||
|
||||
@@ -26,6 +35,7 @@ ShellRoot {
|
||||
property color urgent: "#a55555"
|
||||
|
||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
property string activeTab: "layout"
|
||||
|
||||
// Bundled fallback so 'Reset to defaults' never produces an empty bar even
|
||||
// if bar-defaults.json fails to load. Keep in rough sync with the layout
|
||||
@@ -216,59 +226,98 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
|
||||
// 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" }
|
||||
// Catalog is derived live from BarWidgetRegistry (first-party + third-party
|
||||
// plugin widgets registered at runtime) plus a small legacy descriptor map
|
||||
// for builtins that Bar.qml renders inline via builtinModuleComponent and
|
||||
// hasn't migrated into the registry yet. The merged catalog is rebuilt
|
||||
// whenever the registry revision changes.
|
||||
readonly property var legacyWidgetMeta: ({
|
||||
"omarchy": { name: "Omarchy menu", description: "Launches the Omarchy menu", category: "Compositor" },
|
||||
"workspaces": { name: "Workspaces (legacy)", description: "Workspace numbers, no animation", category: "Compositor" },
|
||||
"clock": { name: "Clock", description: "Date / time text", category: "Time" },
|
||||
"weather": { name: "Weather (legacy)", description: "Tiny weather pill", category: "Info" },
|
||||
"update": { name: "Updates", description: "Indicates available system updates", category: "System" },
|
||||
"voxtype": { name: "Voxtype", description: "Voxtype dictation state", category: "Status" },
|
||||
"screenRecording": { name: "Screen recording", description: "Active recording indicator", category: "Status" },
|
||||
"idle": { name: "Idle (legacy)", description: "Inhibitor indicator", category: "Status" },
|
||||
"notifications": { name: "DND (mako)", description: "Notification silencing indicator", category: "Status" },
|
||||
"tray": { name: "System tray", description: "Status notifier items", category: "Status" },
|
||||
"bluetooth": { name: "Bluetooth (legacy)", description: "Bluetooth status icon", category: "Network" },
|
||||
"network": { name: "Network (legacy)", description: "Wi-Fi / ethernet status", category: "Network" },
|
||||
"audio": { name: "Volume (legacy)", description: "Speaker icon, scroll for volume", category: "Audio" },
|
||||
"cpu": { name: "CPU (legacy)", description: "btop launcher", category: "System" },
|
||||
"battery": { name: "Battery", description: "Battery percent and ETA", category: "System" }
|
||||
})
|
||||
|
||||
property int catalogRevision: 0
|
||||
// Bump on every registry assignment (including the initial null → instance
|
||||
// injection from Loader.onLoaded) so bindings that derive from
|
||||
// widgetMetadata pick up the new state.
|
||||
onBarWidgetRegistryChanged: catalogRevision++
|
||||
Connections {
|
||||
target: root.barWidgetRegistry
|
||||
function onChanged() {
|
||||
root.catalogRevision++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function widgetMetadata(id) {
|
||||
var key = String(id || "")
|
||||
if (root.barWidgetRegistry && root.barWidgetRegistry.has(key))
|
||||
return root.barWidgetRegistry.metadataFor(key) || {}
|
||||
if (legacyWidgetMeta[key]) return legacyWidgetMeta[key]
|
||||
return {}
|
||||
}
|
||||
|
||||
function widgetName(id) {
|
||||
return catalog[id] ? catalog[id].name : id
|
||||
var rev = catalogRevision
|
||||
var meta = widgetMetadata(id)
|
||||
return meta.displayName || meta.name || id
|
||||
}
|
||||
|
||||
function widgetDescription(id) {
|
||||
return catalog[id] ? (catalog[id].description || "") : ""
|
||||
var rev = catalogRevision
|
||||
var meta = widgetMetadata(id)
|
||||
return meta.description || ""
|
||||
}
|
||||
|
||||
function widgetSchema(id) {
|
||||
var meta = widgetMetadata(id)
|
||||
return Array.isArray(meta.schema) ? meta.schema : []
|
||||
}
|
||||
|
||||
function widgetHasSettings(id) {
|
||||
return !!(catalog[id] && catalog[id].settingsForm)
|
||||
var rev = catalogRevision
|
||||
var meta = widgetMetadata(id)
|
||||
if (meta.settingsForm) return true
|
||||
return widgetSchema(id).length > 0
|
||||
}
|
||||
|
||||
function widgetIsPlugin(id) {
|
||||
var meta = widgetMetadata(id)
|
||||
return meta.source === "plugin" || String(id).indexOf("plugin:") === 0
|
||||
}
|
||||
|
||||
function widgetAllowsMultiple(id) {
|
||||
var meta = widgetMetadata(id)
|
||||
if (meta.allowMultiple === true) return true
|
||||
return String(id) === "spacer"
|
||||
}
|
||||
|
||||
function catalogIds() {
|
||||
var rev = catalogRevision
|
||||
var ids = {}
|
||||
if (root.barWidgetRegistry) {
|
||||
var registered = root.barWidgetRegistry.availableIds()
|
||||
for (var i = 0; i < registered.length; i++) ids[registered[i]] = true
|
||||
}
|
||||
for (var key in legacyWidgetMeta) ids[key] = true
|
||||
return Object.keys(ids)
|
||||
}
|
||||
|
||||
function availableToAdd(section) {
|
||||
var rev = catalogRevision
|
||||
var existingByOther = {}
|
||||
var sections = ["left", "center", "right"]
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
@@ -280,26 +329,26 @@ ShellRoot {
|
||||
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) {
|
||||
var ids = catalogIds().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
|
||||
if (!widgetAllowsMultiple(id) && existingHere[id]) continue
|
||||
result.push({ id: id, name: widgetName(id), description: widgetDescription(id), elsewhere: !!existingByOther[id] })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("bar-settings paths",
|
||||
"omarchyPath=" + root.omarchyPath,
|
||||
console.log("bar-settings open. omarchyPath=" + root.omarchyPath,
|
||||
"defaultsPath=" + root.defaultsPath,
|
||||
"userConfigPath=" + root.userConfigPath)
|
||||
"userConfigPath=" + root.userConfigPath,
|
||||
"registry has",
|
||||
root.barWidgetRegistry ? root.barWidgetRegistry.availableIds().length : "(null)",
|
||||
"widgets")
|
||||
}
|
||||
|
||||
FileView {
|
||||
@@ -424,24 +473,57 @@ ShellRoot {
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
TabButton {
|
||||
label: "Layout"
|
||||
selected: root.activeTab === "layout"
|
||||
onClicked: root.activeTab = "layout"
|
||||
}
|
||||
TabButton {
|
||||
label: "Plugins"
|
||||
selected: root.activeTab === "plugins"
|
||||
onClicked: root.activeTab = "plugins"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
contentHeight: activeTabContent.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
ColumnLayout {
|
||||
id: bodyColumn
|
||||
id: activeTabContent
|
||||
width: bodyScroll.width
|
||||
spacing: 14
|
||||
|
||||
SectionEditor { sectionKey: "left"; sectionLabel: "Left" }
|
||||
SectionEditor { sectionKey: "center"; sectionLabel: "Center" }
|
||||
SectionEditor { sectionKey: "right"; sectionLabel: "Right" }
|
||||
ColumnLayout {
|
||||
visible: root.activeTab === "layout"
|
||||
Layout.fillWidth: true
|
||||
spacing: 14
|
||||
|
||||
SectionEditor { sectionKey: "left"; sectionLabel: "Left" }
|
||||
SectionEditor { sectionKey: "center"; sectionLabel: "Center" }
|
||||
SectionEditor { sectionKey: "right"; sectionLabel: "Right" }
|
||||
}
|
||||
|
||||
PluginManager {
|
||||
visible: root.activeTab === "plugins"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -843,13 +925,24 @@ ShellRoot {
|
||||
}
|
||||
|
||||
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
|
||||
var meta = widgetMetadata(id)
|
||||
if (meta && meta.settingsForm) {
|
||||
switch (meta.settingsForm) {
|
||||
case "spacerSettings": return spacerSettingsComponent
|
||||
case "calendarSettings": return calendarSettingsComponent
|
||||
case "brightnessSettings": return brightnessSettingsComponent
|
||||
}
|
||||
}
|
||||
if (widgetSchema(id).length > 0) return dynamicSettingsComponent
|
||||
return null
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dynamicSettingsComponent
|
||||
SettingsUi.DynamicSettingsForm {
|
||||
schema: root.widgetSchema(entry.id || "")
|
||||
foregroundColor: root.foreground
|
||||
fontFamilyName: root.fontFamily
|
||||
}
|
||||
}
|
||||
|
||||
@@ -960,4 +1053,249 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TabButton: Rectangle {
|
||||
id: tab
|
||||
property string label: ""
|
||||
property bool selected: false
|
||||
signal clicked()
|
||||
|
||||
implicitWidth: tabLabel.implicitWidth + 28
|
||||
implicitHeight: 32
|
||||
color: tabArea.containsMouse ? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.08) : "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
id: tabLabel
|
||||
anchors.centerIn: parent
|
||||
text: tab.label
|
||||
color: tab.selected ? root.foreground : Qt.darker(root.foreground, 1.6)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: tab.selected
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 2
|
||||
color: tab.selected ? root.foreground : "transparent"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: tab.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
component PluginManager: Column {
|
||||
id: pm
|
||||
|
||||
property int registryRevision: root.pluginRegistry ? root.pluginRegistry.registryRevision : 0
|
||||
Connections {
|
||||
target: root.pluginRegistry
|
||||
function onPluginsChanged() { pm.registryRevision = root.pluginRegistry.registryRevision }
|
||||
}
|
||||
|
||||
function pluginList() {
|
||||
var rev = pm.registryRevision
|
||||
if (!root.pluginRegistry) return []
|
||||
var plugins = root.pluginRegistry.installedPlugins
|
||||
var ids = Object.keys(plugins).sort(function(a, b) {
|
||||
var fa = !!plugins[a].__isFirstParty, fb = !!plugins[b].__isFirstParty
|
||||
if (fa !== fb) return fa ? -1 : 1
|
||||
return String(plugins[a].name || a).localeCompare(String(plugins[b].name || b))
|
||||
})
|
||||
var rows = []
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var id = ids[i]
|
||||
var m = plugins[id]
|
||||
rows.push({
|
||||
id: id,
|
||||
manifest: m,
|
||||
enabled: root.pluginRegistry.isEnabled(id),
|
||||
firstParty: !!m.__isFirstParty
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
width: parent ? parent.width : 0
|
||||
|
||||
Row {
|
||||
spacing: 8
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: "Plugins"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "· " + (root.pluginRegistry ? Object.keys(root.pluginRegistry.installedPlugins).length : 0) + " installed"
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: parent.width - 280; height: 1 }
|
||||
|
||||
ActionPill {
|
||||
text: "Rescan"
|
||||
onClicked: root.pluginRegistry.rescan()
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Drop plugins at ~/.config/omarchy/plugins/<plugin-id>/"
|
||||
color: Qt.darker(root.foreground, 1.6)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: pm.pluginList()
|
||||
delegate: PluginRow {
|
||||
required property var modelData
|
||||
width: pm.width
|
||||
manifest: modelData.manifest
|
||||
pluginId: modelData.id
|
||||
pluginEnabled: modelData.enabled
|
||||
firstParty: modelData.firstParty
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: pm.pluginList().length === 0
|
||||
text: "No plugins discovered yet."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
component PluginRow: Rectangle {
|
||||
id: row
|
||||
property var manifest: ({})
|
||||
property string pluginId: ""
|
||||
property bool pluginEnabled: false
|
||||
property bool firstParty: false
|
||||
property bool expanded: false
|
||||
|
||||
radius: 4
|
||||
color: rowArea.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
|
||||
implicitHeight: rowContent.implicitHeight + 16
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Column {
|
||||
id: rowContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 8
|
||||
spacing: 4
|
||||
|
||||
Row {
|
||||
spacing: 8
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
width: parent.width - 110
|
||||
|
||||
Text {
|
||||
text: row.manifest && row.manifest.name ? row.manifest.name : row.pluginId
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
text: {
|
||||
var bits = []
|
||||
if (row.manifest && row.manifest.version) bits.push("v" + row.manifest.version)
|
||||
if (row.manifest && row.manifest.author) bits.push(row.manifest.author)
|
||||
bits.push(row.firstParty ? "first-party" : "third-party")
|
||||
return bits.join(" · ")
|
||||
}
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Text {
|
||||
visible: !!(row.manifest && row.manifest.description)
|
||||
text: row.manifest ? (row.manifest.description || "") : ""
|
||||
color: Qt.darker(root.foreground, 1.3)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: 8; height: 1 }
|
||||
|
||||
Switch {
|
||||
checked: row.pluginEnabled
|
||||
enabled: !row.firstParty
|
||||
opacity: row.firstParty ? 0.5 : 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onToggled: root.pluginRegistry.setEnabled(row.pluginId, checked)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: !!(row.manifest && row.manifest.barWidget && Array.isArray(row.manifest.barWidget.schema) && row.manifest.barWidget.schema.length > 0)
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: row.expanded ? "▾ Options" : "▸ Options"
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: row.expanded = !row.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: row.expanded && row.manifest && row.manifest.barWidget && Array.isArray(row.manifest.barWidget.schema) ? row.manifest.barWidget.schema : []
|
||||
delegate: Text {
|
||||
required property var modelData
|
||||
text: "• " + (modelData.label || modelData.key) + " (" + (modelData.type || "string") + ")"
|
||||
color: Qt.darker(root.foreground, 1.3)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
leftPadding: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.bar-settings",
|
||||
"name": "Bar settings",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Visual customizer for the Omarchy bar",
|
||||
"kinds": ["panel"],
|
||||
"activation": "on-demand",
|
||||
"entryPoints": { "panel": "BarSettingsPanel.qml" }
|
||||
}
|
||||
@@ -10,14 +10,15 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "../../services" as Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// The omarchy-shell host injects omarchyPath when it instantiates this Bar.
|
||||
// Default fallback keeps the file loadable in isolation (e.g. for QML tooling).
|
||||
required property string omarchyPath
|
||||
// Injected by the host shell. Shared with the bar-settings panel so both
|
||||
// see the same widget catalogue.
|
||||
required property var barWidgetRegistry
|
||||
property string home: Quickshell.env("HOME")
|
||||
property string omarchyConfigDir: home + "/.config/omarchy"
|
||||
property var builtinBarConfig: ({
|
||||
@@ -306,9 +307,9 @@ Item {
|
||||
"audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false },
|
||||
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false },
|
||||
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false },
|
||||
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false },
|
||||
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" },
|
||||
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND (replaces mako)", category: "Status", allowMultiple: false },
|
||||
"brightness": { displayName: "Brightness", description: "Screen brightness slider", category: "System", allowMultiple: false },
|
||||
"brightness": { displayName: "Brightness", description: "Screen brightness slider", category: "System", allowMultiple: false, settingsForm: "brightnessSettings" },
|
||||
"powerProfile": { displayName: "Power profile", description: "power-profiles-daemon selector", category: "System", allowMultiple: false },
|
||||
"systemStats": { displayName: "System stats", description: "Inline CPU + memory sparklines", category: "System", allowMultiple: false },
|
||||
"weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false },
|
||||
@@ -320,7 +321,7 @@ Item {
|
||||
"nightLight": { displayName: "Night light", description: "hyprsunset toggle", category: "System", allowMultiple: false },
|
||||
"keyboardLayout": { displayName: "Keyboard layout", description: "Current xkb layout, click cycles", category: "Compositor", allowMultiple: false },
|
||||
"lockKeys": { displayName: "Lock keys", description: "Caps / Num / Scroll lock indicators", category: "System", allowMultiple: false },
|
||||
"spacer": { displayName: "Spacer", description: "Configurable blank space", category: "Layout", allowMultiple: true },
|
||||
"spacer": { displayName: "Spacer", description: "Configurable blank space", category: "Layout", allowMultiple: true, settingsForm: "spacerSettings" },
|
||||
"controlCenter": { displayName: "Quick settings", description: "Volume / brightness / DND in one popup", category: "System", allowMultiple: false }
|
||||
})
|
||||
|
||||
@@ -332,7 +333,7 @@ Item {
|
||||
var ids = Object.keys(firstPartyWidgetMetadata)
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var id = ids[i]
|
||||
if (Services.BarWidgetRegistry.has(id)) continue
|
||||
if (barWidgetRegistry.has(id)) continue
|
||||
registerOneFirstPartyWidget(id)
|
||||
}
|
||||
}
|
||||
@@ -345,12 +346,13 @@ Item {
|
||||
description: meta.description || "",
|
||||
category: meta.category || "Misc",
|
||||
allowMultiple: meta.allowMultiple === true,
|
||||
settingsForm: meta.settingsForm || "",
|
||||
source: "first-party"
|
||||
}
|
||||
var comp = Qt.createComponent(url, Component.Asynchronous)
|
||||
function finalize() {
|
||||
if (comp.status === Component.Ready) {
|
||||
Services.BarWidgetRegistry.register(id, comp, enrichedMeta)
|
||||
barWidgetRegistry.register(id, comp, enrichedMeta)
|
||||
var next = ({})
|
||||
for (var k in registeredFirstPartyComponents) next[k] = registeredFirstPartyComponents[k]
|
||||
next[id] = comp
|
||||
@@ -1179,7 +1181,7 @@ Item {
|
||||
// plugin enabled/disabled, etc.). Reading the `widgets` property creates
|
||||
// the binding dependency — the wrapped function call alone wouldn't.
|
||||
readonly property var registryComponent: {
|
||||
var w = Services.BarWidgetRegistry.widgets
|
||||
var w = root.barWidgetRegistry.widgets
|
||||
if (customType || builtinComponent) return null
|
||||
return w[moduleName] ? w[moduleName].component : null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pragma Singleton
|
||||
|
||||
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
|
||||
// settings panel from seeing what the bar registered.
|
||||
QtObject {
|
||||
id: registry
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
// Instance, not a singleton — see BarWidgetRegistry for rationale.
|
||||
QtObject {
|
||||
id: registry
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module omarchy.services
|
||||
singleton PluginRegistry 1.0 PluginRegistry.qml
|
||||
singleton BarWidgetRegistry 1.0 BarWidgetRegistry.qml
|
||||
PluginRegistry 1.0 PluginRegistry.qml
|
||||
BarWidgetRegistry 1.0 BarWidgetRegistry.qml
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import QtQuick
|
||||
import QtQml.Models
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import "plugins/bar"
|
||||
import "services" as Services
|
||||
import "services"
|
||||
|
||||
ShellRoot {
|
||||
id: shell
|
||||
|
||||
// Shared service instances. Plugins receive these via property injection
|
||||
// rather than re-importing them as singletons — relative-path imports do
|
||||
// not share singleton state, which silently leaves consumers with their
|
||||
// own empty copies.
|
||||
property PluginRegistry pluginRegistry: PluginRegistry { }
|
||||
property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { }
|
||||
|
||||
property string home: Quickshell.env("HOME")
|
||||
|
||||
// The omarchy-shell host is the long-running entry point. Plugins live in
|
||||
@@ -30,16 +38,181 @@ ShellRoot {
|
||||
"omarchyPath=" + shell.omarchyPath,
|
||||
"shellDir=" + Quickshell.shellDir,
|
||||
"firstPartyPluginsDir=" + shell.firstPartyPluginsDir)
|
||||
Services.PluginRegistry.firstPartyDir = shell.firstPartyPluginsDir
|
||||
pluginRegistry.firstPartyDir = shell.firstPartyPluginsDir
|
||||
// PluginRegistry.ensureUserDir() runs in its own Component.onCompleted and
|
||||
// chains rescan() once the directory exists. We also kick a scan here in
|
||||
// case the user dir already existed at startup.
|
||||
Services.PluginRegistry.rescan()
|
||||
pluginRegistry.rescan()
|
||||
}
|
||||
|
||||
Bar {
|
||||
id: bar
|
||||
omarchyPath: shell.omarchyPath
|
||||
barWidgetRegistry: shell.barWidgetRegistry
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- on-demand panels
|
||||
|
||||
// openPanelIds is a plain object treated as a set. A plugin id maps to
|
||||
// `true` while the panel is summoned; deleting the key (well, building a new
|
||||
// object without it) hides it. Reassigning the whole object is required for
|
||||
// QML to notice the change.
|
||||
property var openPanelIds: ({})
|
||||
property var panelCache: ({})
|
||||
|
||||
function isPanelOpen(id) { return openPanelIds[id] === true }
|
||||
|
||||
// Pending payloads to deliver to a plugin's open() once its loader resolves.
|
||||
// Keyed by plugin id; consumed by the Loader.onLoaded handler below.
|
||||
property var pendingPayloads: ({})
|
||||
|
||||
function summon(pluginId, payloadJson) {
|
||||
var id = String(pluginId || "")
|
||||
if (!id) return false
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
if (!plugins[id]) {
|
||||
console.warn("summon: unknown plugin", id)
|
||||
return false
|
||||
}
|
||||
var next = ({})
|
||||
for (var k in openPanelIds) next[k] = openPanelIds[k]
|
||||
next[id] = true
|
||||
openPanelIds = next
|
||||
|
||||
// Stash payload so the Loader.onLoaded handler can hand it to open().
|
||||
var pending = ({})
|
||||
for (var p in pendingPayloads) pending[p] = pendingPayloads[p]
|
||||
pending[id] = payloadJson || ""
|
||||
pendingPayloads = pending
|
||||
|
||||
// If the plugin is keepLoaded and already mounted, deliver immediately.
|
||||
deliverIfLoaded(id)
|
||||
return true
|
||||
}
|
||||
|
||||
function hide(pluginId) {
|
||||
var id = String(pluginId || "")
|
||||
if (!id) return false
|
||||
invokeIfLoaded(id, "close", null)
|
||||
if (!openPanelIds[id]) return true
|
||||
var next = ({})
|
||||
for (var k in openPanelIds) if (k !== id) next[k] = openPanelIds[k]
|
||||
openPanelIds = next
|
||||
return true
|
||||
}
|
||||
|
||||
function toggle(pluginId, payloadJson) {
|
||||
var id = String(pluginId || "")
|
||||
return openPanelIds[id] ? hide(id) : summon(id, payloadJson)
|
||||
}
|
||||
|
||||
// Map of pluginId -> Loader, populated by the Instantiator delegate below.
|
||||
property var panelLoaders: ({})
|
||||
|
||||
function registerPanelLoader(pluginId, loader) {
|
||||
var next = ({})
|
||||
for (var k in panelLoaders) next[k] = panelLoaders[k]
|
||||
next[pluginId] = loader
|
||||
panelLoaders = next
|
||||
deliverIfLoaded(pluginId)
|
||||
}
|
||||
|
||||
function unregisterPanelLoader(pluginId) {
|
||||
if (!panelLoaders[pluginId]) return
|
||||
var next = ({})
|
||||
for (var k in panelLoaders) if (k !== pluginId) next[k] = panelLoaders[k]
|
||||
panelLoaders = next
|
||||
}
|
||||
|
||||
function deliverIfLoaded(pluginId) {
|
||||
var loader = panelLoaders[pluginId]
|
||||
if (!loader || !loader.item) return
|
||||
var payload = pendingPayloads[pluginId]
|
||||
if (payload === undefined) return
|
||||
if (typeof loader.item.open === "function") {
|
||||
try { loader.item.open(payload) } catch (e) {
|
||||
console.warn("plugin " + pluginId + " open() threw:", e)
|
||||
}
|
||||
}
|
||||
var next = ({})
|
||||
for (var k in pendingPayloads) if (k !== pluginId) next[k] = pendingPayloads[k]
|
||||
pendingPayloads = next
|
||||
}
|
||||
|
||||
function invokeIfLoaded(pluginId, method, arg) {
|
||||
var loader = panelLoaders[pluginId]
|
||||
if (!loader || !loader.item) return
|
||||
if (typeof loader.item[method] !== "function") return
|
||||
try { loader.item[method](arg) } catch (e) {
|
||||
console.warn("plugin " + pluginId + " " + method + "() threw:", e)
|
||||
}
|
||||
}
|
||||
|
||||
// One Loader per discoverable panel/overlay/menu plugin. Active when the
|
||||
// host marks it open. The Loader holds onto the instance while active so the
|
||||
// plugin's FloatingWindow + state survive between summons within a session.
|
||||
property var panelEntries: []
|
||||
|
||||
function computePanelEntries() {
|
||||
var out = []
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
var panelKinds = ["panel", "overlay", "menu"]
|
||||
for (var id in plugins) {
|
||||
var m = plugins[id]
|
||||
if (!m || !Array.isArray(m.kinds)) continue
|
||||
var matched = false
|
||||
for (var i = 0; i < panelKinds.length; i++)
|
||||
if (m.kinds.indexOf(panelKinds[i]) !== -1) { matched = true; break }
|
||||
if (!matched) continue
|
||||
if (!shell.pluginRegistry.isEnabled(id)) continue
|
||||
var kind = m.kinds.indexOf("panel") !== -1 ? "panel"
|
||||
: (m.kinds.indexOf("overlay") !== -1 ? "overlay" : "menu")
|
||||
out.push({ id: id, manifest: m, kind: kind, keepLoaded: m.keepLoaded === true })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell.pluginRegistry
|
||||
function onPluginsChanged() { shell.panelEntries = shell.computePanelEntries() }
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: shell.panelEntries
|
||||
active: true
|
||||
|
||||
delegate: QtObject {
|
||||
id: panelEntry
|
||||
required property var modelData
|
||||
readonly property string pluginId: modelData.id
|
||||
readonly property var manifest: modelData.manifest
|
||||
readonly property string entryKind: modelData.kind
|
||||
readonly property bool keepLoaded: modelData.keepLoaded === true
|
||||
readonly property string sourceUrl: shell.pluginRegistry.entryPointUrl(manifest, entryKind)
|
||||
|
||||
property Loader panelLoader: Loader {
|
||||
source: panelEntry.sourceUrl
|
||||
active: panelEntry.sourceUrl !== "" && (panelEntry.keepLoaded || shell.openPanelIds[panelEntry.pluginId] === true)
|
||||
asynchronous: true
|
||||
onLoaded: {
|
||||
if (!item) return
|
||||
if ("omarchyPath" in item) item.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in item) item.shell = shell
|
||||
if ("manifest" in item) item.manifest = panelEntry.manifest
|
||||
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistry
|
||||
shell.registerPanelLoader(panelEntry.pluginId, this)
|
||||
}
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
console.warn("panel plugin " + panelEntry.pluginId + " failed:",
|
||||
sourceComponent ? sourceComponent.errorString() : "")
|
||||
shell.hide(panelEntry.pluginId)
|
||||
}
|
||||
}
|
||||
Component.onDestruction: shell.unregisterPanelLoader(panelEntry.pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- plugin loader
|
||||
@@ -48,32 +221,32 @@ ShellRoot {
|
||||
// Each enabled plugin with kind "bar-widget" gets a Component created from
|
||||
// its manifest entry point and registered under the id "plugin:<id>".
|
||||
Connections {
|
||||
target: Services.PluginRegistry
|
||||
target: shell.pluginRegistry
|
||||
function onPluginsChanged() { shell.syncPluginWidgets() }
|
||||
}
|
||||
|
||||
property var pluginWidgetComponents: ({})
|
||||
|
||||
function syncPluginWidgets() {
|
||||
var plugins = Services.PluginRegistry.installedPlugins
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
var seen = ({})
|
||||
|
||||
for (var pluginId in plugins) {
|
||||
var manifest = plugins[pluginId]
|
||||
if (!manifest || !manifest.kinds || manifest.kinds.indexOf("bar-widget") === -1) continue
|
||||
if (!Services.PluginRegistry.isEnabled(pluginId)) continue
|
||||
if (!shell.pluginRegistry.isEnabled(pluginId)) continue
|
||||
|
||||
var registryKey = "plugin:" + manifest.id
|
||||
seen[registryKey] = true
|
||||
|
||||
// Already loaded with matching source — leave it alone.
|
||||
var existing = pluginWidgetComponents[registryKey]
|
||||
var url = Services.PluginRegistry.entryPointUrl(manifest, "barWidget")
|
||||
var url = shell.pluginRegistry.entryPointUrl(manifest, "barWidget")
|
||||
if (!url) {
|
||||
console.warn("Plugin " + manifest.id + " has no barWidget entry point")
|
||||
continue
|
||||
}
|
||||
if (existing && existing.url === url && Services.BarWidgetRegistry.has(registryKey)) continue
|
||||
if (existing && existing.url === url && shell.barWidgetRegistry.has(registryKey)) continue
|
||||
|
||||
var meta = manifest.barWidget || {}
|
||||
meta = {
|
||||
@@ -91,12 +264,12 @@ ShellRoot {
|
||||
}
|
||||
|
||||
// Drop registrations for plugins that are no longer present or enabled.
|
||||
var allIds = Services.BarWidgetRegistry.availableIds()
|
||||
var allIds = shell.barWidgetRegistry.availableIds()
|
||||
for (var i = 0; i < allIds.length; i++) {
|
||||
var id = allIds[i]
|
||||
if (id.indexOf("plugin:") !== 0) continue
|
||||
if (!seen[id]) {
|
||||
Services.BarWidgetRegistry.unregister(id)
|
||||
shell.barWidgetRegistry.unregister(id)
|
||||
var next = ({})
|
||||
for (var k in pluginWidgetComponents) if (k !== id) next[k] = pluginWidgetComponents[k]
|
||||
pluginWidgetComponents = next
|
||||
@@ -108,14 +281,14 @@ ShellRoot {
|
||||
var comp = Qt.createComponent(url, Component.Asynchronous)
|
||||
function finalize() {
|
||||
if (comp.status === Component.Ready) {
|
||||
Services.BarWidgetRegistry.register(registryKey, comp, meta)
|
||||
shell.barWidgetRegistry.register(registryKey, comp, meta)
|
||||
var next = ({})
|
||||
for (var k in pluginWidgetComponents) next[k] = pluginWidgetComponents[k]
|
||||
next[registryKey] = { url: url, component: comp }
|
||||
pluginWidgetComponents = next
|
||||
} else if (comp.status === Component.Error) {
|
||||
console.warn("Plugin widget " + registryKey + " failed: " + comp.errorString())
|
||||
Services.PluginRegistry.pluginLoadFailed(registryKey, comp.errorString())
|
||||
shell.pluginRegistry.pluginLoadFailed(registryKey, comp.errorString())
|
||||
}
|
||||
}
|
||||
if (comp.status === Component.Loading) {
|
||||
@@ -135,26 +308,38 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function rescanPlugins(): void {
|
||||
Services.PluginRegistry.rescan()
|
||||
shell.pluginRegistry.rescan()
|
||||
}
|
||||
|
||||
function setPluginEnabled(id: string, enabled: string): void {
|
||||
Services.PluginRegistry.setEnabled(id, enabled === "true")
|
||||
shell.pluginRegistry.setEnabled(id, enabled === "true")
|
||||
}
|
||||
|
||||
function listPlugins(): string {
|
||||
var out = []
|
||||
var plugins = Services.PluginRegistry.installedPlugins
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
for (var id in plugins) {
|
||||
out.push({
|
||||
id: id,
|
||||
name: plugins[id].name,
|
||||
kinds: plugins[id].kinds,
|
||||
enabled: Services.PluginRegistry.isEnabled(id),
|
||||
enabled: shell.pluginRegistry.isEnabled(id),
|
||||
firstParty: !!plugins[id].__isFirstParty
|
||||
})
|
||||
}
|
||||
return JSON.stringify(out)
|
||||
}
|
||||
|
||||
function summon(id: string, payloadJson: string): string {
|
||||
return shell.summon(id, payloadJson) ? "ok" : "unknown"
|
||||
}
|
||||
|
||||
function hide(id: string): void {
|
||||
shell.hide(id)
|
||||
}
|
||||
|
||||
function toggle(id: string, payloadJson: string): void {
|
||||
shell.toggle(id, payloadJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
// Generic schema-driven settings form. The host panel passes:
|
||||
// - schema: array of { key, label, type, defaultValue?, options?, min?, max?, step?, description? }
|
||||
// - entry: current widget entry object (set by the loader); fields override defaults
|
||||
// - signal fieldChanged(string key, var value) — emitted on user edits
|
||||
//
|
||||
// Supported types: boolean, enum, integer, number, string, path, command, color.
|
||||
Column {
|
||||
id: root
|
||||
|
||||
signal fieldChanged(string key, var value)
|
||||
|
||||
property var schema: []
|
||||
property var entry: ({})
|
||||
property color foregroundColor: "#cacccc"
|
||||
property string fontFamilyName: "JetBrainsMono Nerd Font"
|
||||
spacing: 10
|
||||
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] : ""
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.schema
|
||||
|
||||
Column {
|
||||
required property var modelData
|
||||
width: root.width
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: modelData && modelData.label ? modelData.label : (modelData && modelData.key ? modelData.key : "")
|
||||
color: Qt.darker(root.foregroundColor, 1.3)
|
||||
font.family: root.fontFamilyName
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !!(modelData && modelData.description)
|
||||
text: modelData ? (modelData.description || "") : ""
|
||||
color: Qt.darker(root.foregroundColor, 1.6)
|
||||
font.family: root.fontFamilyName
|
||||
font.pixelSize: 10
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Loader {
|
||||
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 "color":
|
||||
case "string":
|
||||
case "path":
|
||||
case "command":
|
||||
default: return stringField
|
||||
}
|
||||
}
|
||||
onLoaded: if (item && "fieldKey" in item) {
|
||||
item.fieldKey = modelData.key
|
||||
item.field = modelData
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: stringField
|
||||
TextField {
|
||||
property string fieldKey: ""
|
||||
property var field: ({})
|
||||
width: parent.width
|
||||
font.family: root.fontFamilyName
|
||||
font.pixelSize: 12
|
||||
text: root.currentValue(field) === undefined ? "" : String(root.currentValue(field))
|
||||
onEditingFinished: if (fieldKey) root.fieldChanged(fieldKey, text)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: booleanField
|
||||
CheckBox {
|
||||
property string fieldKey: ""
|
||||
property var field: ({})
|
||||
font.family: root.fontFamilyName
|
||||
font.pixelSize: 12
|
||||
text: field && field.label ? "" : ""
|
||||
checked: !!root.currentValue(field)
|
||||
onToggled: if (fieldKey) root.fieldChanged(fieldKey, checked)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: enumField
|
||||
ComboBox {
|
||||
property string fieldKey: ""
|
||||
property var field: ({})
|
||||
width: parent.width
|
||||
font.family: root.fontFamilyName
|
||||
font.pixelSize: 12
|
||||
model: field && field.options ? field.options : []
|
||||
currentIndex: {
|
||||
var v = root.currentValue(field)
|
||||
for (var i = 0; i < (field && field.options ? field.options.length : 0); i++)
|
||||
if (field.options[i] === v) return i
|
||||
return 0
|
||||
}
|
||||
onActivated: function(index) {
|
||||
if (fieldKey && field && field.options) root.fieldChanged(fieldKey, field.options[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: integerField
|
||||
SpinBox {
|
||||
property string fieldKey: ""
|
||||
property var field: ({})
|
||||
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
|
||||
}
|
||||
onValueModified: if (fieldKey) root.fieldChanged(fieldKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: numberField
|
||||
Row {
|
||||
property string fieldKey: ""
|
||||
property var field: ({})
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
property real currentNumber: {
|
||||
var v = root.currentValue(field)
|
||||
var n = typeof v === "number" ? v : parseFloat(String(v || 0))
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
Slider {
|
||||
id: slider
|
||||
width: parent.width - readout.width - 8
|
||||
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.foregroundColor
|
||||
font.family: root.fontFamilyName
|
||||
font.pixelSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.schema || root.schema.length === 0
|
||||
text: "No settings."
|
||||
color: Qt.darker(root.foregroundColor, 1.5)
|
||||
font.family: root.fontFamilyName
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
module omarchy.ui.settings
|
||||
DynamicSettingsForm 1.0 DynamicSettingsForm.qml
|
||||
Reference in New Issue
Block a user