Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-4
This commit is contained in:
+23
-14
@@ -75,13 +75,15 @@ Supported `kinds`:
|
||||
|
||||
| Kind | What it is |
|
||||
|--------------|--------------------------------------------------------------|
|
||||
| `bar-widget` | A component that the bar can drop into a section |
|
||||
| `bar-widget` | A component that the active bar can drop into a section |
|
||||
| `panel` | A persistent or summoned floating window (e.g. OSD) |
|
||||
| `overlay` | A fullscreen overlay (e.g. background switcher) |
|
||||
| `menu` | A summoned menu surface |
|
||||
| `service` | A headless singleton, no UI |
|
||||
| `bar` | Reserved for the first-party bar host (`omarchy.bar`). Third-party plugins should ship `bar-widget`s; they do not replace the host bar. |
|
||||
| `bar` | A full bar option that can replace the built-in `omarchy.bar` |
|
||||
|
||||
Only one `bar` plugin is active at a time. Missing or invalid selections fall
|
||||
back to the built-in `omarchy.bar`, so users always have a safe path home.
|
||||
Panels, overlays, and menus are loaded when summoned. Plugins that need
|
||||
to outlive a single summon can set `keepLoaded: true` (e.g. the image
|
||||
picker keeps its overlay window mounted between summons). First-party
|
||||
@@ -132,7 +134,7 @@ You can still drop a plugin in without a source:
|
||||
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
|
||||
plus the QML referenced from its `entryPoints`.
|
||||
2. `omarchy plugin rescan`.
|
||||
3. `omarchy plugin enable <id>` (bar widgets also need `omarchy plugin bar add <id>`).
|
||||
3. `omarchy plugin enable <id>` (bar widgets also need `omarchy plugin bar add <id>`; full bar replacements are selected with `omarchy plugin bar use <id>`).
|
||||
|
||||
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
|
||||
`omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`.
|
||||
@@ -151,7 +153,9 @@ omarchy plugin edit local.clock --with ai # edit with `omarchy launch ai`
|
||||
```
|
||||
|
||||
First-party plugins under `shell/plugins/`
|
||||
are discovered the same way and cannot be disabled.
|
||||
are discovered the same way and cannot be disabled, except that the built-in
|
||||
bar option can become inactive while a third-party `kind: "bar"` plugin is the
|
||||
selected bar.
|
||||
|
||||
## IPC contract
|
||||
|
||||
@@ -224,6 +228,7 @@ becomes the authoritative file — we do **not** deep-merge defaults back in.
|
||||
"lock": 300
|
||||
},
|
||||
"bar": {
|
||||
"id": "omarchy.bar",
|
||||
"position": "top",
|
||||
"transparent": false,
|
||||
"centerAnchor": "omarchy.clock",
|
||||
@@ -241,27 +246,31 @@ becomes the authoritative file — we do **not** deep-merge defaults back in.
|
||||
|
||||
### Storage rules
|
||||
|
||||
1. **Every plugin instance is one entry.** Either in `bar.layout.<section>`
|
||||
1. **The active bar option is `bar.id`.** Omit it or set it to `omarchy.bar`
|
||||
to use the built-in bar. Set it to another plugin id whose manifest declares
|
||||
`kind: "bar"` to replace the full bar.
|
||||
2. **Every plugin instance is one entry.** Either in `bar.layout.<section>`
|
||||
for bar widgets, or in `plugins[]` for panels, overlays, services,
|
||||
menus, and anything else non-bar.
|
||||
2. **Settings are inline on the entry.** No `config:` sub-object, no
|
||||
3. **Settings are inline on the entry.** No `config:` sub-object, no
|
||||
separate per-plugin settings file, no merge layers. The fields on each
|
||||
entry are the values the plugin sees.
|
||||
3. **Built-in widget ids are namespaced.** Use ids such as `omarchy.clock`,
|
||||
4. **Built-in widget ids are namespaced.** Use ids such as `omarchy.clock`,
|
||||
`omarchy.audio`, and `omarchy.network`. The migration rewrites older ids
|
||||
like `Clock` and `AudioPanel` forward.
|
||||
4. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff
|
||||
its id appears somewhere in shell.json. For bar widgets, the bar
|
||||
settings UI adds/removes layout entries; other plugin kinds are enabled
|
||||
with the shell IPC. First-party plugins are always enabled.
|
||||
5. **Multiple instances** are allowed when a manifest sets
|
||||
5. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff
|
||||
its id appears somewhere in shell.json. For full bar options, that means
|
||||
`bar.id`; for bar widgets, the bar settings UI adds/removes layout entries;
|
||||
other plugin kinds are enabled with the shell IPC. First-party non-bar
|
||||
plugins are always enabled.
|
||||
6. **Multiple instances** are allowed when a manifest sets
|
||||
`allowMultiple: true`. Each instance is independent — e.g. two clock
|
||||
widgets in different timezones are just two `{"id":"omarchy.clock", "timezone": ...}`
|
||||
entries with their own values.
|
||||
6. **Idle timings are top-level.** `idle.screensaver` and `idle.lock`
|
||||
7. **Idle timings are top-level.** `idle.screensaver` and `idle.lock`
|
||||
are seconds since user idle began, so the default lock fires at 300s
|
||||
even if the 150s screensaver starts first.
|
||||
7. **`version: 1` is required** at the top level. The shell will fall back
|
||||
8. **`version: 1` is required** at the top level. The shell will fall back
|
||||
to defaults rather than load an unknown version.
|
||||
|
||||
## Implementation history
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
These plugins ship with Omarchy and are discovered 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 are always enabled. Services and keep-loaded panels are mounted at
|
||||
startup; other panels, overlays, and menus are loaded on demand.
|
||||
only difference is that the shell flags them with `__isFirstParty: true`.
|
||||
First-party non-bar plugins are always enabled; `omarchy.bar` is the default
|
||||
bar option and becomes inactive only while a third-party `kind: "bar"` plugin is
|
||||
selected. Services and keep-loaded panels are mounted at startup; other panels,
|
||||
overlays, and menus are loaded on demand.
|
||||
|
||||
User-installed plugins live alongside these conceptually but on disk under
|
||||
`~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory.
|
||||
@@ -40,7 +42,7 @@ own plugin directories, each with its own `manifest.json`.
|
||||
|
||||
## Bar
|
||||
|
||||
The status bar. Mounted at startup, lives forever. Layout lives in the
|
||||
The built-in status bar and default full-bar option. Layout lives in the
|
||||
top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell
|
||||
providing [`config/omarchy/shell.json`](../../config/omarchy/shell.json) when
|
||||
the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue
|
||||
|
||||
@@ -22,6 +22,9 @@ Item {
|
||||
// Injected by the host shell. Used for shell-wide actions such as opening
|
||||
// settings and persisting inline widget state.
|
||||
property var shell: null
|
||||
// Manifest for the active bar option. Present for custom bars and useful for
|
||||
// diagnostics; the built-in bar does not otherwise need it.
|
||||
property var manifest: null
|
||||
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
|
||||
// killing the entire shell. Wired to BarPanel.visible below; updated by the
|
||||
// FileView watcher further down.
|
||||
|
||||
@@ -97,29 +97,41 @@ QtObject {
|
||||
}
|
||||
|
||||
// Enabled = the plugin id is referenced somewhere in shell.json. That can
|
||||
// be either a layout entry inside `bar.layout.*` (bar widgets) or a top-level
|
||||
// entry in `plugins[]` (panels, overlays, services).
|
||||
// be either the active bar option in `bar.id`, a layout entry inside
|
||||
// `bar.layout.*` (bar widgets), or a top-level entry in `plugins[]` (panels,
|
||||
// overlays, services).
|
||||
//
|
||||
// Special cases (implicitly always enabled, no shell.json entry needed):
|
||||
// - plugins whose `kinds` contains "bar" are mounted directly by the host.
|
||||
// - first-party plugins are shell infrastructure (settings,
|
||||
// - the built-in bar option (`omarchy.bar`) is active when `bar.id` is
|
||||
// missing or set to `omarchy.bar`.
|
||||
// - first-party non-bar plugins are shell infrastructure (settings,
|
||||
// image-picker, ...). Requiring users to add them to plugins[] just to
|
||||
// summon them was a footgun: a stock shell.json with `plugins: []` would
|
||||
// silently make `omarchy launch bar-settings` a no-op.
|
||||
function isEnabled(id) {
|
||||
var key = String(id)
|
||||
var manifest = installedPlugins[key]
|
||||
var config = shellConfigProvider ? shellConfigProvider() : null
|
||||
if (manifest) {
|
||||
if (Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1) return true
|
||||
if (Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1) {
|
||||
var selectedBar = ""
|
||||
if (Util.isPlainObject(config) && Util.isPlainObject(config.bar))
|
||||
selectedBar = Util.canonicalWidgetId(String(config.bar.id || ""))
|
||||
if (!selectedBar) selectedBar = "omarchy.bar"
|
||||
return selectedBar === key
|
||||
}
|
||||
if (manifest.__isFirstParty) return true
|
||||
}
|
||||
var config = shellConfigProvider ? shellConfigProvider() : null
|
||||
return findEntryLocation(config, key).found
|
||||
}
|
||||
|
||||
function findEntryLocation(config, id) {
|
||||
if (!Util.isPlainObject(config)) return { found: false }
|
||||
var key = Util.canonicalWidgetId(String(id))
|
||||
if (Util.isPlainObject(config.bar)) {
|
||||
var selectedBar = Util.canonicalWidgetId(String(config.bar.id || ""))
|
||||
if (selectedBar === key) return { found: true, kind: "bar-option" }
|
||||
}
|
||||
if (Util.isPlainObject(config.bar) && Util.isPlainObject(config.bar.layout)) {
|
||||
var sections = ["left", "center", "right"]
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
@@ -148,12 +160,23 @@ QtObject {
|
||||
return
|
||||
}
|
||||
var manifest = installedPlugins[key]
|
||||
var isBarOption = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1
|
||||
var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
|
||||
shellConfigMutator(function(config) {
|
||||
// Ensure shape exists.
|
||||
if (!Util.isPlainObject(config.bar)) config.bar = { layout: { left: [], center: [], right: [] } }
|
||||
if (!Util.isPlainObject(config.bar.layout)) config.bar.layout = { left: [], center: [], right: [] }
|
||||
if (!Array.isArray(config.plugins)) config.plugins = []
|
||||
|
||||
if (isBarOption) {
|
||||
if (value) {
|
||||
config.bar.id = key
|
||||
} else if (Util.canonicalWidgetId(String(config.bar.id || "")) === key) {
|
||||
delete config.bar.id
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var location = findEntryLocation(config, key)
|
||||
if (value && !location.found) {
|
||||
var entry = { id: key }
|
||||
@@ -166,7 +189,7 @@ QtObject {
|
||||
} else if (!value && location.found) {
|
||||
if (location.kind === "bar") {
|
||||
config.bar.layout[location.section].splice(location.index, 1)
|
||||
} else {
|
||||
} else if (location.kind === "plugin") {
|
||||
config.plugins.splice(location.index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
+103
-10
@@ -56,6 +56,7 @@ ShellRoot {
|
||||
property bool suppressUserReload: false
|
||||
|
||||
onShellConfigChanged: {
|
||||
if (failedBarId !== "") failedBarId = ""
|
||||
pluginRegistry.registryRevision++
|
||||
pluginRegistry.pluginsChanged()
|
||||
}
|
||||
@@ -105,6 +106,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
readonly property var barConfig: shellConfig && Util.isPlainObject(shellConfig.bar) ? shellConfig.bar : builtinShellConfig.bar
|
||||
onBarConfigChanged: if (bar && "barConfig" in bar) bar.barConfig = shell.barConfig
|
||||
FileView {
|
||||
id: defaultsFile
|
||||
path: shell.defaultsPath
|
||||
@@ -159,15 +161,102 @@ ShellRoot {
|
||||
}
|
||||
|
||||
// Exposed as a property so child plugins (notifications, future panels)
|
||||
// can read barSize/barHidden/position to anchor relative to the bar.
|
||||
property alias bar: bar
|
||||
// can read barSize/barHidden/position to anchor relative to the active bar.
|
||||
readonly property string defaultBarId: "omarchy.bar"
|
||||
readonly property string selectedBarId: {
|
||||
var config = shell.barConfig
|
||||
if (Util.isPlainObject(config)) {
|
||||
var configured = Util.canonicalWidgetId(String(config.id || ""))
|
||||
if (configured) return configured
|
||||
}
|
||||
return shell.defaultBarId
|
||||
}
|
||||
property string failedBarId: ""
|
||||
readonly property bool selectedBarAvailable: {
|
||||
var revision = shell.pluginRegistry.registryRevision
|
||||
return shell.barOptionAvailable(shell.selectedBarId)
|
||||
}
|
||||
readonly property string activeBarId: selectedBarId !== failedBarId && selectedBarAvailable ? selectedBarId : defaultBarId
|
||||
readonly property var activeBarManifest: {
|
||||
var revision = shell.pluginRegistry.registryRevision
|
||||
return shell.barManifestFor(shell.activeBarId)
|
||||
}
|
||||
readonly property string activeBarSourceUrl: activeBarId === defaultBarId ? "" : shell.pluginRegistry.entryPointUrl(activeBarManifest, "bar")
|
||||
property var bar: null
|
||||
|
||||
Bar {
|
||||
id: bar
|
||||
omarchyPath: shell.omarchyPath
|
||||
barWidgetRegistry: shell.barWidgetRegistry
|
||||
barConfig: shell.barConfig
|
||||
shell: shell
|
||||
onSelectedBarIdChanged: if (failedBarId !== "") failedBarId = ""
|
||||
|
||||
function barManifestFor(pluginId) {
|
||||
var plugins = shell.pluginRegistry ? shell.pluginRegistry.installedPlugins : null
|
||||
return plugins ? plugins[String(pluginId || "")] || null : null
|
||||
}
|
||||
|
||||
function isBarOptionManifest(manifest) {
|
||||
return manifest
|
||||
&& Array.isArray(manifest.kinds)
|
||||
&& manifest.kinds.indexOf("bar") !== -1
|
||||
&& manifest.entryPoints
|
||||
&& manifest.entryPoints.bar
|
||||
}
|
||||
|
||||
function barOptionAvailable(pluginId) {
|
||||
var id = String(pluginId || "")
|
||||
if (id === "" || id === shell.defaultBarId) return true
|
||||
var manifest = shell.barManifestFor(id)
|
||||
return shell.isBarOptionManifest(manifest) && shell.pluginRegistry.entryPointUrl(manifest, "bar") !== ""
|
||||
}
|
||||
|
||||
function isActiveBarOption(pluginId) {
|
||||
return String(pluginId || "") === shell.activeBarId
|
||||
}
|
||||
|
||||
function configureBar(target, manifest) {
|
||||
if (!target) return
|
||||
if ("omarchyPath" in target) target.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in target) target.shell = shell
|
||||
if ("manifest" in target) target.manifest = manifest
|
||||
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistry
|
||||
if ("barConfig" in target) target.barConfig = shell.barConfig
|
||||
shell.bar = target
|
||||
}
|
||||
|
||||
Component {
|
||||
id: defaultBarComponent
|
||||
|
||||
Bar {
|
||||
omarchyPath: shell.omarchyPath
|
||||
barWidgetRegistry: shell.barWidgetRegistry
|
||||
barConfig: shell.barConfig
|
||||
shell: shell
|
||||
manifest: shell.barManifestFor(shell.defaultBarId)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: defaultBarLoader
|
||||
|
||||
active: shell.activeBarId === shell.defaultBarId
|
||||
sourceComponent: defaultBarComponent
|
||||
onLoaded: shell.configureBar(item, shell.barManifestFor(shell.defaultBarId))
|
||||
onActiveChanged: if (!active && shell.activeBarId !== shell.defaultBarId) shell.bar = null
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: pluginBarLoader
|
||||
|
||||
active: shell.activeBarId !== shell.defaultBarId && shell.activeBarSourceUrl !== ""
|
||||
source: shell.activeBarId !== shell.defaultBarId ? shell.activeBarSourceUrl : ""
|
||||
asynchronous: true
|
||||
onLoaded: shell.configureBar(item, shell.activeBarManifest)
|
||||
onActiveChanged: if (!active && shell.activeBarId === shell.defaultBarId) shell.bar = null
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
var detail = errorString && errorString() ? errorString() : ""
|
||||
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId + ":", detail)
|
||||
shell.failedBarId = shell.activeBarId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- services
|
||||
@@ -639,11 +728,15 @@ ShellRoot {
|
||||
var out = []
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
for (var id in plugins) {
|
||||
var kinds = plugins[id].kinds || []
|
||||
var isBarOption = Array.isArray(kinds) && kinds.indexOf("bar") !== -1
|
||||
var active = isBarOption && shell.isActiveBarOption(id)
|
||||
out.push({
|
||||
id: id,
|
||||
name: plugins[id].name,
|
||||
kinds: plugins[id].kinds,
|
||||
enabled: shell.pluginRegistry.isEnabled(id),
|
||||
kinds: kinds,
|
||||
enabled: isBarOption ? active : shell.pluginRegistry.isEnabled(id),
|
||||
active: active,
|
||||
firstParty: !!plugins[id].__isFirstParty
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user