Polish docs and harden plugin loading
This commit is contained in:
@@ -101,6 +101,45 @@ For interactive UI work, use `wtype` to simulate keyboard input when available.
|
|||||||
|
|
||||||
When testing layer-shell UI, capture the reference and candidate states as separate screenshots, then compare them visually before further edits. If a launched UI would otherwise remain open, keep track of its PID and stop it after the screenshot; avoid broad process kills unless checking with `ps` first.
|
When testing layer-shell UI, capture the reference and candidate states as separate screenshots, then compare them visually before further edits. If a launched UI would otherwise remain open, keep track of its PID and stop it after the screenshot; avoid broad process kills unless checking with `ps` first.
|
||||||
|
|
||||||
|
# Omarchy shell
|
||||||
|
|
||||||
|
The Quickshell desktop runs as a single long-running process out of
|
||||||
|
`default/quickshell/omarchy-shell/`. Hyprland's autostart launches it; do
|
||||||
|
not start additional standalone `quickshell -p` instances for individual
|
||||||
|
components.
|
||||||
|
|
||||||
|
Plugin contract:
|
||||||
|
|
||||||
|
- Each plugin lives in its own directory under
|
||||||
|
`default/quickshell/omarchy-shell/plugins/<id>/` (first-party) or
|
||||||
|
`~/.config/omarchy/plugins/<id>/` (third-party).
|
||||||
|
- Every plugin ships a `manifest.json` declaring `id`, `kinds`,
|
||||||
|
`activation`, and `entryPoints`. The full schema is in
|
||||||
|
`default/quickshell/omarchy-shell/README.md`.
|
||||||
|
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the
|
||||||
|
shell-injected properties `omarchyPath`, `shell`, `manifest`, and
|
||||||
|
`pluginRegistry` / `barWidgetRegistry` as appropriate.
|
||||||
|
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
||||||
|
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
||||||
|
|
||||||
|
IPC:
|
||||||
|
|
||||||
|
- `bin/omarchy-shell-ipc` is the canonical IPC entry point. It starts
|
||||||
|
the shell on first call, then forwards to `quickshell ipc call`.
|
||||||
|
Prefer it over re-implementing the wait-for-instance dance in every
|
||||||
|
CLI.
|
||||||
|
- The `shell` IPC target exposes `ping`, `summon`, `hide`, `toggle`,
|
||||||
|
`rescanPlugins`, `setPluginEnabled`, and `listPlugins`. Individual
|
||||||
|
plugins can register additional IPC targets (the bar registers `bar`,
|
||||||
|
the background switcher registers `image-selector`).
|
||||||
|
|
||||||
|
Widget files in `plugins/bar/widgets/` contain Nerd Font glyphs as raw
|
||||||
|
unicode characters. The `Write` and `Edit` tools strip multi-byte
|
||||||
|
codepoints in some positions — do **not** rewrite widget files wholesale
|
||||||
|
through those tools. For glyph fixes, use the targeted `Edit` tool with
|
||||||
|
the surrounding context, or a Python script that inserts codepoints via
|
||||||
|
`chr(0xXXXXX)`.
|
||||||
|
|
||||||
# Refresh Pattern
|
# Refresh Pattern
|
||||||
|
|
||||||
To copy a default config to user config with automatic backup:
|
To copy a default config to user config with automatic backup:
|
||||||
|
|||||||
+29
-7
@@ -3,15 +3,37 @@
|
|||||||
# omarchy:summary=Send an IPC call to omarchy-shell, starting it if not running
|
# omarchy:summary=Send an IPC call to omarchy-shell, starting it if not running
|
||||||
# omarchy:hidden=true
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
if [[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]]; then
|
||||||
|
cat <<USAGE
|
||||||
|
Usage: omarchy-shell-ipc <target> <method> [args...]
|
||||||
|
|
||||||
|
Starts omarchy-shell if not running, then forwards a quickshell ipc call.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
omarchy-shell-ipc shell ping
|
||||||
|
omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
|
||||||
|
omarchy-shell-ipc shell hide omarchy.background-switcher
|
||||||
|
omarchy-shell-ipc shell listPlugins
|
||||||
|
omarchy-shell-ipc shell rescanPlugins
|
||||||
|
USAGE
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}"
|
OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}"
|
||||||
SHELL_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell"
|
SHELL_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell"
|
||||||
|
|
||||||
if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then
|
# Serialize concurrent invocations so two CLI callers don't both spawn the shell
|
||||||
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$SHELL_DIR" >/dev/null 2>&1 &
|
# when no instance is running yet.
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
lockfile="${XDG_RUNTIME_DIR:-/tmp}/omarchy-shell-ipc.lock"
|
||||||
sleep 0.2
|
{
|
||||||
quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance ' && break
|
flock 9
|
||||||
done
|
if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then
|
||||||
fi
|
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
|
||||||
|
} 9>"$lockfile"
|
||||||
|
|
||||||
exec quickshell ipc -p "$SHELL_DIR" call "$@"
|
exec quickshell ipc -p "$SHELL_DIR" call "$@"
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# Omarchy shell
|
||||||
|
|
||||||
|
`omarchy-shell` is a single long-running [Quickshell](https://quickshell.org/)
|
||||||
|
instance that hosts the Omarchy desktop. Hyprland autostarts one shell per
|
||||||
|
session; everything else — the bar, the bar settings UI, the background
|
||||||
|
switcher, future panels and overlays — runs **inside** the shell as a
|
||||||
|
plugin.
|
||||||
|
|
||||||
|
Hosting everything inside one shell means:
|
||||||
|
|
||||||
|
- shared services and singletons live once, not once per process
|
||||||
|
- summoning a panel is an IPC call into a process that is already running,
|
||||||
|
not a fresh `quickshell -p ...` cold start
|
||||||
|
- third-party plugins can be loaded from disk without changing any source
|
||||||
|
code in Omarchy itself
|
||||||
|
|
||||||
|
The runtime layout in this branch:
|
||||||
|
|
||||||
|
```
|
||||||
|
default/quickshell/omarchy-shell/
|
||||||
|
shell.qml entry point (ShellRoot)
|
||||||
|
services/
|
||||||
|
PluginRegistry.qml discovers, validates, persists plugin state
|
||||||
|
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
|
||||||
|
ui/
|
||||||
|
settings/
|
||||||
|
DynamicSettingsForm.qml renders plugin-declared schemas
|
||||||
|
plugins/
|
||||||
|
bar/ first-party plugins (see plugins/README.md)
|
||||||
|
bar-settings/
|
||||||
|
background-switcher/
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin discovery path is documented in [plugins/README.md](plugins/README.md).
|
||||||
|
|
||||||
|
## Plugin manifest
|
||||||
|
|
||||||
|
Every plugin ships a `manifest.json` describing what it is and how the
|
||||||
|
shell should load it. Minimal example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "my.org.cool-clock",
|
||||||
|
"name": "Cool clock",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "You",
|
||||||
|
"description": "A clock that does cool things",
|
||||||
|
"kinds": ["bar-widget"],
|
||||||
|
"activation": "on-demand",
|
||||||
|
"entryPoints": { "barWidget": "Widget.qml" },
|
||||||
|
"barWidget": {
|
||||||
|
"displayName": "Cool clock",
|
||||||
|
"category": "Time",
|
||||||
|
"allowMultiple": false,
|
||||||
|
"defaults": { "format": "HH:mm" },
|
||||||
|
"schema": [
|
||||||
|
{ "key": "format", "type": "string", "label": "Format" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported `kinds`:
|
||||||
|
|
||||||
|
| Kind | What it is |
|
||||||
|
|--------------|--------------------------------------------------------------|
|
||||||
|
| `bar-widget` | A component that the bar can drop into a section |
|
||||||
|
| `panel` | A persistent or summoned floating window (e.g. bar settings) |
|
||||||
|
| `overlay` | A fullscreen overlay (e.g. background switcher) |
|
||||||
|
| `menu` | A summoned menu surface |
|
||||||
|
| `service` | A headless singleton, no UI |
|
||||||
|
|
||||||
|
`activation` is either `persistent` (loaded on startup, never unloaded) or
|
||||||
|
`on-demand` (loaded by `shell summon <id>` and unloaded by `shell hide`).
|
||||||
|
Plugins that need their IPC socket to outlive a single summon can set
|
||||||
|
`keepLoaded: true` (e.g. background-switcher's legacy unix socket).
|
||||||
|
|
||||||
|
The full schema lives in `services/PluginRegistry.qml`.
|
||||||
|
|
||||||
|
## Installing a third-party plugin
|
||||||
|
|
||||||
|
1. Drop the plugin into `~/.config/omarchy/plugins/<plugin-id>/`.
|
||||||
|
The directory must contain a `manifest.json` plus the QML files
|
||||||
|
referenced from its `entryPoints`.
|
||||||
|
2. `omarchy-shell-ipc shell rescanPlugins` — or open the Plugin Manager
|
||||||
|
tab in `omarchy launch bar-settings` and click **Rescan**.
|
||||||
|
3. Enable the plugin (Plugin Manager **Enable** toggle, or
|
||||||
|
`omarchy-shell-ipc shell setPluginEnabled <id> true`).
|
||||||
|
4. If it's a `bar-widget`, add it to a layout section from the bar editor.
|
||||||
|
|
||||||
|
First-party plugins under `default/quickshell/omarchy-shell/plugins/`
|
||||||
|
are discovered the same way and cannot be disabled.
|
||||||
|
|
||||||
|
## IPC contract
|
||||||
|
|
||||||
|
The shell exposes a single `shell` IPC target plus whatever extra targets
|
||||||
|
individual plugins register (e.g. the bar's `bar` target for refresh
|
||||||
|
hooks, the background switcher's `image-selector` target).
|
||||||
|
|
||||||
|
| Method | Returns | Effect |
|
||||||
|
|------------------------------------------|---------|-------------------------------------------------------|
|
||||||
|
| `ping` | `ok` | health check |
|
||||||
|
| `summon <id> <payloadJson>` | `ok` / `unknown` | load + open a panel/overlay plugin |
|
||||||
|
| `hide <id>` | — | close a previously-summoned plugin |
|
||||||
|
| `toggle <id> <payloadJson>` | — | summon if closed, hide if open |
|
||||||
|
| `rescanPlugins` | — | re-walk plugin dirs and pick up new/changed manifests |
|
||||||
|
| `setPluginEnabled <id> <enabled>` | — | flip the persisted enabled bit (see note) |
|
||||||
|
| `listPlugins` | JSON | every discovered plugin (id, name, kinds, enabled) |
|
||||||
|
|
||||||
|
Direct invocation:
|
||||||
|
|
||||||
|
```
|
||||||
|
quickshell ipc -p $OMARCHY_PATH/default/quickshell/omarchy-shell call shell ping
|
||||||
|
```
|
||||||
|
|
||||||
|
A convenience wrapper, [`omarchy-shell-ipc`](../../../bin/omarchy-shell-ipc),
|
||||||
|
starts the shell if it is not already running, then forwards a `call`. It
|
||||||
|
is the canonical way for other Omarchy CLIs to talk to the shell.
|
||||||
|
|
||||||
|
```
|
||||||
|
omarchy-shell-ipc shell ping
|
||||||
|
omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
|
||||||
|
omarchy-shell-ipc shell listPlugins
|
||||||
|
omarchy-shell-ipc shell rescanPlugins
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note on `setPluginEnabled`:** the `enabled` argument is a string. Only the
|
||||||
|
literal `"true"` enables the plugin; every other value (including `"True"`,
|
||||||
|
`"1"`, `"yes"`, or omitted) disables it. This keeps the IPC surface
|
||||||
|
type-stable across QML's `string`-only IPC arguments.
|
||||||
|
|
||||||
|
## Persisted state
|
||||||
|
|
||||||
|
| Path | Owner | Purpose |
|
||||||
|
|-------------------------------------------|----------------|--------------------------------------|
|
||||||
|
| `~/.config/omarchy/bar.json` | bar plugin | section layout + per-entry settings |
|
||||||
|
| `~/.config/omarchy/plugins.json` | PluginRegistry | enabled/disabled state |
|
||||||
|
| `~/.config/omarchy/plugins/<id>/` | user | manifest + entry points + assets |
|
||||||
|
| `~/.config/omarchy/plugins/<id>/settings.json` | user | optional per-plugin overrides |
|
||||||
|
|
||||||
|
## Implementation history
|
||||||
|
|
||||||
|
Built up in phases on this branch:
|
||||||
|
|
||||||
|
- Phase 1 — `omarchy-shell phase 1: host the existing bar in a single shell`
|
||||||
|
- Phase 2 — `omarchy-shell phase 2: plugin registry and bar widget registry`
|
||||||
|
- Phase 3 — `omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin`
|
||||||
|
- Phase 4 — `omarchy-shell phase 4: absorb background-switcher as a plugin`
|
||||||
|
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs` (this commit)
|
||||||
|
|
||||||
|
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
|
||||||
|
out of scope here and deferred to a follow-up after a review pass.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# First-party plugins
|
||||||
|
|
||||||
|
These plugins ship with Omarchy and are loaded by the shell at startup.
|
||||||
|
They use the same `manifest.json` contract as third-party plugins; the
|
||||||
|
only difference is that the shell flags them with `__isFirstParty: true`
|
||||||
|
so they cannot be disabled.
|
||||||
|
|
||||||
|
User-installed plugins live alongside these conceptually but on disk under
|
||||||
|
`~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory.
|
||||||
|
|
||||||
|
| Plugin | id | kinds | activation | entry point |
|
||||||
|
|-----------------------|-------------------------------|--------------|-------------|----------------------------|
|
||||||
|
| Bar | `omarchy.bar` | `bar` | persistent | `bar/Bar.qml` |
|
||||||
|
| Bar settings | `omarchy.bar-settings` | `panel` | on-demand | `bar-settings/BarSettingsPanel.qml` |
|
||||||
|
| Background switcher | `omarchy.background-switcher` | `overlay` | on-demand | `background-switcher/BackgroundSwitcher.qml` |
|
||||||
|
|
||||||
|
## Bar
|
||||||
|
|
||||||
|
The status bar. Mounted at startup, lives forever. Layout is configured
|
||||||
|
through `~/.config/omarchy/bar.json` (deep-merged over
|
||||||
|
[`bar/bar-defaults.json`](bar/bar-defaults.json)). Owns the `bar` IPC
|
||||||
|
target for refresh hooks fired by indicator scripts. See
|
||||||
|
[`bar/README.md`](bar/README.md) for the widget catalogue and customization
|
||||||
|
schema.
|
||||||
|
|
||||||
|
## Bar settings
|
||||||
|
|
||||||
|
Visual editor for the bar layout. Summoned by
|
||||||
|
`omarchy-shell-ipc shell summon omarchy.bar-settings "{}"` (which is what
|
||||||
|
`omarchy launch bar-settings` ultimately calls). Provides:
|
||||||
|
|
||||||
|
- per-section add/move/remove/edit of widget entries
|
||||||
|
- a Plugin Manager tab for enabling/disabling third-party plugins
|
||||||
|
- a dynamic settings form driven by each widget's manifest schema
|
||||||
|
|
||||||
|
## Background switcher
|
||||||
|
|
||||||
|
Fullscreen wallpaper / image picker overlay. Summoned for ad-hoc wallpaper
|
||||||
|
selection. Keeps its legacy unix socket protocol at
|
||||||
|
`/run/user/<uid>/omarchy-image-selector.sock` so existing callers like
|
||||||
|
`omarchy-menu-images` keep working without any wire-format change. The
|
||||||
|
plugin has `keepLoaded: true` so the socket survives between summons.
|
||||||
|
|
||||||
|
## Coming soon
|
||||||
|
|
||||||
|
- `omarchy.menu` — folds the existing `omarchy-menu` (currently on another
|
||||||
|
branch) into the shell as a `menu` plugin. Not present on this branch.
|
||||||
|
- `omarchy.theme-switcher` — folds theme switching into the shell.
|
||||||
+40
-4
@@ -5,9 +5,19 @@ import QtQuick
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
|
|
||||||
ShellRoot {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Injected by omarchy-shell. Optional here — the picker doesn't need
|
||||||
|
// omarchyPath itself, but every plugin gets it so user-installed scripts
|
||||||
|
// referenced by other plugins can stay path-portable.
|
||||||
|
property string omarchyPath: ""
|
||||||
|
// Set by omarchy-shell when summoning the overlay; not currently consumed but
|
||||||
|
// declared so the host's onLoaded injection doesn't trip a missing-property
|
||||||
|
// warning.
|
||||||
|
property var shell: null
|
||||||
|
property var manifest: null
|
||||||
|
|
||||||
property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds")
|
property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds")
|
||||||
property string imageRows: ""
|
property string imageRows: ""
|
||||||
property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE")
|
property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE")
|
||||||
@@ -293,9 +303,35 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
// The plugin is keep-loaded inside omarchy-shell, so the env-driven
|
||||||
if (selectionFile)
|
// auto-open path that the standalone background-switcher.qml used would
|
||||||
openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile, "", false, false)
|
// now fire once at shell startup with stale env. The legacy callers
|
||||||
|
// (omarchy-menu-images) deliver their request over the unix socket
|
||||||
|
// declared below; modern callers go through `shell summon` -> open(payload).
|
||||||
|
|
||||||
|
// Lifecycle hooks invoked by omarchy-shell summon/hide. The legacy entry
|
||||||
|
// point remains the unix socket below — callers that already have a
|
||||||
|
// selection_file/done_file flow keep using it. summon() with no payload
|
||||||
|
// simply opens the picker against the user's current theme backgrounds.
|
||||||
|
function open(payload) {
|
||||||
|
var args = {}
|
||||||
|
if (payload) {
|
||||||
|
try { args = JSON.parse(payload) || {} } catch (e) { args = {} }
|
||||||
|
}
|
||||||
|
var dirs = String(args.imageDirs || imageDirs)
|
||||||
|
var rows = String(args.imageRows || "")
|
||||||
|
var sel = String(args.selectedImage || selectedImage)
|
||||||
|
var selFile = String(args.selectionFile || "")
|
||||||
|
var doneF = String(args.doneFile || "")
|
||||||
|
var colors = String(args.colorsFile || colorsFile)
|
||||||
|
var colorsRaw = String(args.colorsRaw || "")
|
||||||
|
var labels = args.showLabels === true || args.showLabels === "true"
|
||||||
|
var filter = args.filterable === true || args.filterable === "true"
|
||||||
|
openSelector(dirs, rows, sel, selFile, doneF, colors, colorsRaw, labels, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import "../../ui/settings" as SettingsUi
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Plugin lifecycle hooks. omarchy-shell calls open(payloadJson) on summon
|
||||||
|
// and close() on hide. We don't consume payloads yet, and visibility is
|
||||||
|
// driven by the host Loader's `active`, so both are no-ops for now.
|
||||||
|
function open(payloadJson) { /* no payload schema yet; reserved for future use */ }
|
||||||
|
function close() { /* visibility handled by parent Loader; nothing to clean up */ }
|
||||||
|
|
||||||
// Injected by the host shell when the panel is summoned. Shared instances
|
// Injected by the host shell when the panel is summoned. Shared instances
|
||||||
// so the panel sees the same registry state the bar wrote into.
|
// so the panel sees the same registry state the bar wrote into.
|
||||||
property var barWidgetRegistry: null
|
property var barWidgetRegistry: null
|
||||||
@@ -342,12 +348,16 @@ Item {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
// Only log once the registry has actually been injected by the host. The
|
||||||
|
// raw Component.onCompleted fires before the Loader's onLoaded property
|
||||||
|
// injection so it would always print `(null)` widgets, which is noise.
|
||||||
|
onBarWidgetRegistryChanged: {
|
||||||
|
if (!root.barWidgetRegistry) return
|
||||||
console.log("bar-settings open. omarchyPath=" + root.omarchyPath,
|
console.log("bar-settings open. omarchyPath=" + root.omarchyPath,
|
||||||
"defaultsPath=" + root.defaultsPath,
|
"defaultsPath=" + root.defaultsPath,
|
||||||
"userConfigPath=" + root.userConfigPath,
|
"userConfigPath=" + root.userConfigPath,
|
||||||
"registry has",
|
"registry has",
|
||||||
root.barWidgetRegistry ? root.barWidgetRegistry.availableIds().length : "(null)",
|
root.barWidgetRegistry.availableIds().length,
|
||||||
"widgets")
|
"widgets")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1251,12 +1261,33 @@ Item {
|
|||||||
|
|
||||||
Item { width: 8; height: 1 }
|
Item { width: 8; height: 1 }
|
||||||
|
|
||||||
Switch {
|
Item {
|
||||||
checked: row.pluginEnabled
|
implicitWidth: enabledSwitch.implicitWidth
|
||||||
enabled: !row.firstParty
|
implicitHeight: enabledSwitch.implicitHeight
|
||||||
opacity: row.firstParty ? 0.5 : 1
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onToggled: root.pluginRegistry.setEnabled(row.pluginId, checked)
|
|
||||||
|
Switch {
|
||||||
|
id: enabledSwitch
|
||||||
|
checked: row.pluginEnabled
|
||||||
|
enabled: !row.firstParty
|
||||||
|
opacity: row.firstParty ? 0.45 : 1
|
||||||
|
ToolTip.visible: row.firstParty && hoverArea.containsMouse
|
||||||
|
ToolTip.delay: 300
|
||||||
|
ToolTip.text: "First-party plugin — always enabled"
|
||||||
|
onToggled: root.pluginRegistry.setEnabled(row.pluginId, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch.enabled=false also disables mouse tracking, so the tooltip
|
||||||
|
// never sees a hover event. Layer a transparent hover-only MouseArea
|
||||||
|
// on top to surface the explanation.
|
||||||
|
MouseArea {
|
||||||
|
id: hoverArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
visible: row.firstParty
|
||||||
|
cursorShape: Qt.ForbiddenCursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -714,7 +714,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
path: root.omarchyPath + "/default/quickshell/bar/bar-defaults.json"
|
path: root.omarchyPath + "/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json"
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: root.loadDefaultBarConfig(text())
|
onLoaded: root.loadDefaultBarConfig(text())
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
# Omarchy bar
|
# Omarchy bar
|
||||||
|
|
||||||
This is the Quickshell implementation of the Omarchy status bar.
|
This is the Quickshell implementation of the Omarchy status bar. It is
|
||||||
|
shipped as a first-party plugin of [`omarchy-shell`](../../README.md), the
|
||||||
|
long-running shell host. The bar is mounted at startup and lives inside
|
||||||
|
the shell for its whole session.
|
||||||
|
|
||||||
|
- `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`, `activation: persistent`) and points at `Bar.qml` as the entry point.
|
||||||
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
|
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
|
||||||
- `bar-defaults.json` is the Omarchy-owned default layout and module settings.
|
- `bar-defaults.json` is the Omarchy-owned default layout and module settings.
|
||||||
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
|
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
|
||||||
@@ -156,4 +160,13 @@ Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (o
|
|||||||
- `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup
|
- `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup
|
||||||
- `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator
|
- `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator
|
||||||
|
|
||||||
Drop new widgets into `widgets/<name>.qml`, add the name to the `firstPartyWidgets` registry in `shell.qml`, and reference it by name in any layout list.
|
First-party widgets live in `widgets/<name>.qml` and are picked up by the
|
||||||
|
shell's `BarWidgetRegistry` at startup; reference one by `id` in any
|
||||||
|
layout list.
|
||||||
|
|
||||||
|
Third-party widgets ship as separate plugins under
|
||||||
|
`~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json`
|
||||||
|
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
|
||||||
|
[../../README.md](../../README.md) for the manifest schema and the
|
||||||
|
Plugin Manager tab in `omarchy launch bar-settings` for enable/disable
|
||||||
|
controls.
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ QtObject {
|
|||||||
return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
|
return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSafeEntryPoint(value) {
|
||||||
|
if (typeof value !== "string" || value.length === 0) return false
|
||||||
|
if (value.charAt(0) === "/") return false
|
||||||
|
if (value.indexOf("..") !== -1) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function validateManifest(manifest, sourcePath) {
|
function validateManifest(manifest, sourcePath) {
|
||||||
if (!isPlainObject(manifest)) {
|
if (!isPlainObject(manifest)) {
|
||||||
console.warn("PluginRegistry: manifest is not an object at " + sourcePath)
|
console.warn("PluginRegistry: manifest is not an object at " + sourcePath)
|
||||||
@@ -62,6 +69,16 @@ QtObject {
|
|||||||
console.warn("PluginRegistry: entryPoints must be an object at " + sourcePath)
|
console.warn("PluginRegistry: entryPoints must be an object at " + sourcePath)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
// Every entry point must be a relative path inside the plugin's source
|
||||||
|
// directory. Reject the whole manifest if anything looks like an attempt
|
||||||
|
// to escape the plugin's sandbox.
|
||||||
|
for (var key in manifest.entryPoints) {
|
||||||
|
if (!isSafeEntryPoint(manifest.entryPoints[key])) {
|
||||||
|
console.warn("PluginRegistry: unsafe entryPoint '" + key + "'='"
|
||||||
|
+ manifest.entryPoints[key] + "' at " + sourcePath)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
return manifest
|
return manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +88,15 @@ QtObject {
|
|||||||
if (!ep) return ""
|
if (!ep) return ""
|
||||||
var dir = manifest.__sourceDir || ""
|
var dir = manifest.__sourceDir || ""
|
||||||
if (!dir) return ""
|
if (!dir) return ""
|
||||||
return fileUrl(dir + "/" + ep)
|
// Defense in depth: even after validateManifest, confirm the resolved
|
||||||
|
// path stays inside the plugin's sourceDir.
|
||||||
|
var resolved = dir.replace(/\/$/, "") + "/" + String(ep)
|
||||||
|
var expectedPrefix = dir.replace(/\/$/, "") + "/"
|
||||||
|
if (resolved.indexOf(expectedPrefix) !== 0) {
|
||||||
|
console.warn("PluginRegistry: entry point escapes sourceDir: " + resolved)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fileUrl(resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEnabled(id) {
|
function isEnabled(id) {
|
||||||
@@ -194,7 +219,15 @@ QtObject {
|
|||||||
|
|
||||||
var merged = {}
|
var merged = {}
|
||||||
for (var fk in firstParty) merged[fk] = firstParty[fk]
|
for (var fk in firstParty) merged[fk] = firstParty[fk]
|
||||||
for (var tk in thirdParty) merged[tk] = thirdParty[tk]
|
// Third-party plugins never shadow a first-party one with the same id.
|
||||||
|
for (var tk in thirdParty) {
|
||||||
|
if (firstParty[tk]) {
|
||||||
|
console.warn("PluginRegistry: plugin " + tk
|
||||||
|
+ " rejected: id collides with first-party plugin")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
merged[tk] = thirdParty[tk]
|
||||||
|
}
|
||||||
|
|
||||||
pluginStates = nextStates
|
pluginStates = nextStates
|
||||||
installedPlugins = merged
|
installedPlugins = merged
|
||||||
@@ -234,12 +267,12 @@ QtObject {
|
|||||||
+ "}; "
|
+ "}; "
|
||||||
+ "scan \"$0\" firstparty; "
|
+ "scan \"$0\" firstparty; "
|
||||||
+ "scan \"$1\" thirdparty"
|
+ "scan \"$1\" thirdparty"
|
||||||
scanProcess.command = ["bash", "-lc", script, registry.firstPartyDir, registry.pluginsDir]
|
scanProcess.command = ["bash", "-c", script, registry.firstPartyDir, registry.pluginsDir]
|
||||||
scanProcess.running = true
|
scanProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureUserDir() {
|
function ensureUserDir() {
|
||||||
initProcess.command = ["bash", "-lc", "mkdir -p \"$0\"", registry.pluginsDir]
|
initProcess.command = ["bash", "-c", "mkdir -p \"$0\"", registry.pluginsDir]
|
||||||
initProcess.running = true
|
initProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
module omarchy.services
|
|
||||||
PluginRegistry 1.0 PluginRegistry.qml
|
|
||||||
BarWidgetRegistry 1.0 BarWidgetRegistry.qml
|
|
||||||
@@ -58,12 +58,13 @@ ShellRoot {
|
|||||||
// object without it) hides it. Reassigning the whole object is required for
|
// object without it) hides it. Reassigning the whole object is required for
|
||||||
// QML to notice the change.
|
// QML to notice the change.
|
||||||
property var openPanelIds: ({})
|
property var openPanelIds: ({})
|
||||||
property var panelCache: ({})
|
|
||||||
|
|
||||||
function isPanelOpen(id) { return openPanelIds[id] === true }
|
function isPanelOpen(id) { return openPanelIds[id] === true }
|
||||||
|
|
||||||
// Pending payloads to deliver to a plugin's open() once its loader resolves.
|
// Pending payloads to deliver to a plugin's open() once its loader resolves.
|
||||||
// Keyed by plugin id; consumed by the Loader.onLoaded handler below.
|
// Keyed by plugin id; the value is an array so two summon() calls before
|
||||||
|
// the Loader resolves both reach the plugin in arrival order rather than
|
||||||
|
// the second clobbering the first.
|
||||||
property var pendingPayloads: ({})
|
property var pendingPayloads: ({})
|
||||||
|
|
||||||
function summon(pluginId, payloadJson) {
|
function summon(pluginId, payloadJson) {
|
||||||
@@ -81,8 +82,10 @@ ShellRoot {
|
|||||||
|
|
||||||
// Stash payload so the Loader.onLoaded handler can hand it to open().
|
// Stash payload so the Loader.onLoaded handler can hand it to open().
|
||||||
var pending = ({})
|
var pending = ({})
|
||||||
for (var p in pendingPayloads) pending[p] = pendingPayloads[p]
|
for (var p in pendingPayloads) pending[p] = pendingPayloads[p].slice()
|
||||||
pending[id] = payloadJson || ""
|
var queue = pending[id] || []
|
||||||
|
queue.push(payloadJson || "")
|
||||||
|
pending[id] = queue
|
||||||
pendingPayloads = pending
|
pendingPayloads = pending
|
||||||
|
|
||||||
// If the plugin is keepLoaded and already mounted, deliver immediately.
|
// If the plugin is keepLoaded and already mounted, deliver immediately.
|
||||||
@@ -127,15 +130,17 @@ ShellRoot {
|
|||||||
function deliverIfLoaded(pluginId) {
|
function deliverIfLoaded(pluginId) {
|
||||||
var loader = panelLoaders[pluginId]
|
var loader = panelLoaders[pluginId]
|
||||||
if (!loader || !loader.item) return
|
if (!loader || !loader.item) return
|
||||||
var payload = pendingPayloads[pluginId]
|
var queue = pendingPayloads[pluginId]
|
||||||
if (payload === undefined) return
|
if (!Array.isArray(queue) || queue.length === 0) return
|
||||||
if (typeof loader.item.open === "function") {
|
if (typeof loader.item.open === "function") {
|
||||||
try { loader.item.open(payload) } catch (e) {
|
for (var i = 0; i < queue.length; i++) {
|
||||||
console.warn("plugin " + pluginId + " open() threw:", e)
|
try { loader.item.open(queue[i]) } catch (e) {
|
||||||
|
console.warn("plugin " + pluginId + " open() threw:", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var next = ({})
|
var next = ({})
|
||||||
for (var k in pendingPayloads) if (k !== pluginId) next[k] = pendingPayloads[k]
|
for (var k in pendingPayloads) if (k !== pluginId) next[k] = pendingPayloads[k].slice()
|
||||||
pendingPayloads = next
|
pendingPayloads = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
module omarchy.ui.settings
|
|
||||||
DynamicSettingsForm 1.0 DynamicSettingsForm.qml
|
|
||||||
Reference in New Issue
Block a user