From 126d26f390331387e6bd60bcc38775b52131eb71 Mon Sep 17 00:00:00 2001 From: Jeremy Monson Date: Tue, 1 Sep 2026 16:39:04 -0700 Subject: [PATCH] List third-party bar widgets that declare display modes in the Icons editor A third-party bar widget can now opt into the Control Center's Icons appearance editor by declaring the displayMode values it renders in its manifest: "x-shibumi": { "displayModes": ["full", "icon", "text"] }. The plugin catalog carries the validated declaration, the workbench adds one catalog option per declared enabled widget under its dynamic G: group, and the mode chooser offers only the declared modes in V2 (V1 stays at Default, matching the state service's normalization). Widgets without the declaration keep their original rendering and stay in Plugins. Affected plugins: hancore.shibumi.control-center. Checks: tests/control-center-regression.sh (installed-source-parity baseline), tests/documentation-regression.py, git diff --check. The smoke test now adds a flag-gated third-party fixture entry and asserts it is listed with modes icon,full in V2, Default-only in V1, and that an undeclared widget is not listed. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 6 ++ docs/control-center-v4.md | 7 +- docs/plugin-compatibility.md | 18 ++++++ .../ControlCenterPanel.qml | 14 ++++ .../WidgetAppearanceWorkbench.qml | 48 +++++++++++++- tests/control-center-regression.sh | 4 ++ tests/control-center-smoke.qml | 64 +++++++++++++++++++ tests/fixtures/ControlCenterTestPanel.qml | 37 +++++++---- 8 files changed, 179 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a935a38..0d87143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added + +- Listed third-party bar widgets that declare `x-shibumi.displayModes` in the Icons appearance editor, offering only their declared modes + ## 0.1.1-beta.11: Native Audio and Bluetooth cutover Public beta candidate for 2026-08-23. diff --git a/docs/control-center-v4.md b/docs/control-center-v4.md index 0f2f6e7..607f00d 100644 --- a/docs/control-center-v4.md +++ b/docs/control-center-v4.md @@ -411,8 +411,11 @@ not secondary sections inside Icons: - **Icons** owns per-widget icon/content modes plus their surfaces, colors, shape, spacing, and opacity. It follows the active bar's canonical left/center/right layout order and lists only enabled groups that implement - the Shibumi appearance contract. Provider filters do not appear in this - editor; unsupported stock or third-party widgets remain in Plugins. The + the Shibumi appearance contract, plus enabled third-party bar widgets that + declare `x-shibumi.displayModes` (see + [plugin compatibility](plugin-compatibility.md#third-party-widget-appearance)). + Provider filters do not appear in this editor; other stock or third-party + widgets remain in Plugins. The selected widget's live preview shares the inspector header, content and surface choices use visual samples, and the surface palette uses the same neutral-border, underline-selection, and hover-motion contract as Bars. diff --git a/docs/plugin-compatibility.md b/docs/plugin-compatibility.md index ed84ade..8cc31ca 100644 --- a/docs/plugin-compatibility.md +++ b/docs/plugin-compatibility.md @@ -85,6 +85,24 @@ payload verification, and uninstall remain transactional. The application launcher remains owned by Omarchy. Shibumi neither registers a `menu` entry point nor enables an application-menu service. +## Third-party widget appearance + +The Control Center's Icons editor lists Shibumi widgets and any enabled +third-party bar widget whose manifest declares the display modes it renders: + +```json +"x-shibumi": { "displayModes": ["full", "icon", "text"] } +``` + +`full` (icon and text) is the canonical default and must be included; `icon` +and `text` are optional. The editor offers only the declared modes, stores the +choice in the widget's group settings, and the bar host injects it into the +widget's `settings` as `displayMode` (the legacy `compact: true` flag means +`icon`). The V2 presentation honors `icon` and `text`; V1 keeps third-party +widgets at Default. Surface, color, outline, spacing, and opacity controls +apply through the host slot and need no widget support. Widgets that do not +declare `displayModes` keep their original rendering and stay in Plugins. + ## Third-party bar acceptance A third-party bar must implement Omarchy Quattro's standard bar-widget diff --git a/hancore.shibumi.control-center/ControlCenterPanel.qml b/hancore.shibumi.control-center/ControlCenterPanel.qml index 8e988da..c266931 100644 --- a/hancore.shibumi.control-center/ControlCenterPanel.qml +++ b/hancore.shibumi.control-center/ControlCenterPanel.qml @@ -312,6 +312,19 @@ ShibumiPanel { return changed } + // Third-party bar widgets opt into the Icons editor by declaring which + // displayMode values they render: "x-shibumi": { "displayModes": [...] }. + // Unknown values are dropped; a widget that cannot show "full" (the + // canonical default) is not listed. Suite widgets keep their catalog modes. + function pluginDisplayModes(shibumi) { + const declared = shibumi && Array.isArray(shibumi.displayModes) + ? shibumi.displayModes : [] + const result = ["full", "icon", "text"].filter(function(mode) { + return declared.indexOf(mode) >= 0 + }) + return result.indexOf("full") >= 0 ? result : [] + } + function pluginGlyph(pluginId, kinds) { const id = String(pluginId || "").toLowerCase() const semanticGlyphs = [ @@ -488,6 +501,7 @@ ShibumiPanel { version: String(manifest.version || ""), kinds: kinds, glyph: pluginGlyph(id, kinds), + displayModes: pluginDisplayModes(shibumi), enabled: enabled, barWidget: barWidget, installedInBar: barWidget diff --git a/hancore.shibumi.control-center/WidgetAppearanceWorkbench.qml b/hancore.shibumi.control-center/WidgetAppearanceWorkbench.qml index 136dc6d..9f8204c 100644 --- a/hancore.shibumi.control-center/WidgetAppearanceWorkbench.qml +++ b/hancore.shibumi.control-center/WidgetAppearanceWorkbench.qml @@ -8,6 +8,10 @@ Column { required property var controller required property var widgetOptions + // Third-party bar widgets that declare x-shibumi.displayModes join the + // catalog under their dynamic "G:" group. + readonly property var catalogOptions: + widgetOptions.concat(pluginAppearanceOptions()) property real uiScale: 1 property color foreground: Commons.Color.menu.text property color accent: Commons.Color.menu.selectedText @@ -199,6 +203,8 @@ Column { function pluginIdForGroup(groupValue) { const plugin = pluginForGroup(groupValue) if (plugin) return String(plugin.id || "") + if (isPluginCatalogGroup(groupValue)) + return String(groupValue).slice(2) const ids = { G1: "hancore.shibumi.control-center", G2: "hancore.shibumi.workspaces", @@ -336,13 +342,38 @@ Column { function catalogOptionForGroup(groupValue) { const group = String(groupValue || "") - for (let index = 0; index < widgetOptions.length; index++) { - if (String(widgetOptions[index].group || "") === group) - return widgetOptions[index] + for (let index = 0; index < catalogOptions.length; index++) { + if (String(catalogOptions[index].group || "") === group) + return catalogOptions[index] } return null } + function pluginAppearanceOptions() { + const entries = controller.pluginEntries || [] + const result = [] + for (let index = 0; index < entries.length; index++) { + const entry = entries[index] + const id = String(entry.id || "") + const modes = Array.isArray(entry.displayModes) ? entry.displayModes : [] + if (id === "" || entry.barWidget !== true || modes.length === 0 + || controller.shibumiWidgetGroup(id) !== "") continue + result.push({ + group: "G:" + id, + label: String(entry.name || id), + glyph: String(entry.glyph || "widgets"), + modes: modes + }) + } + return result + } + + function isPluginCatalogGroup(groupValue) { + const group = String(groupValue || "") + return group.indexOf("G:") === 0 + && controller.shibumiWidgetGroup(group.slice(2)) === "" + } + function isShibumiWidgetOption(source) { if (!source) return false return pluginIdForGroup(String(source.group || "")) @@ -460,6 +491,17 @@ Column { || catalogGroupForSettingsGroup(groupValue) || "") if (catalogGroup === "G1") return [] if (catalogGroup === "G9") return mediaStyleOptions + if (isPluginCatalogGroup(catalogGroup)) { + // The state service normalizes third-party display modes in V2 only; + // V1 keeps them at Default. + const option = catalogOptionForGroup(catalogGroup) + const modes = option && Array.isArray(option.modes) ? option.modes : [] + if (controller.v2LayoutActive !== true) + return [{ value: "full", label: "Default", enabled: true }] + return displayModeOptions.filter(function(candidate) { + return modes.indexOf(candidate.value) >= 0 + }) + } if (controller.v2LayoutActive === true) return displayModeOptions const compactAvailable = v1CompactGroupIds.indexOf(catalogGroup) >= 0 return compactAvailable ? [ diff --git a/tests/control-center-regression.sh b/tests/control-center-regression.sh index 107b050..75c40fc 100755 --- a/tests/control-center-regression.sh +++ b/tests/control-center-regression.sh @@ -1649,6 +1649,10 @@ workbench="$control_dir/WidgetAppearanceWorkbench.qml" || fail "direct widget Appearance workbench is missing" for workbench_contract in \ 'readonly property var overviewOptions: buildOverviewOptions()' \ + 'widgetOptions.concat(pluginAppearanceOptions())' \ + 'function pluginAppearanceOptions()' \ + 'function isPluginCatalogGroup(groupValue)' \ + '|| controller.shibumiWidgetGroup(id) !== "") continue' \ 'readonly property var activeOptions: overviewOptions.active' \ 'readonly property var inactiveOptions: overviewOptions.inactive' \ 'readonly property var editableOptions: activeOptions.concat(inactiveOptions)' \ diff --git a/tests/control-center-smoke.qml b/tests/control-center-smoke.qml index e12bf42..527a1d1 100644 --- a/tests/control-center-smoke.qml +++ b/tests/control-center-smoke.qml @@ -446,6 +446,70 @@ ShellRoot { - v1OverviewPanelHeight) > 0.5) return root.fail("Icons overview height differed between V1 and V2") panel.v2LayoutActive = false + + // A third-party bar widget that declares x-shibumi.displayModes joins + // the Icons editor with only its declared modes in V2, and stays at + // Default in V1. One without the declaration is not listed. + const savedEntries = panel.pluginEntries + panel.pluginEntries = savedEntries.concat([{ + id: "example.chess", name: "Example Chess", description: "", + author: "Example", category: "Info", searchTags: [], + kinds: ["bar-widget"], glyph: "widgets", provider: "Third-party", + removable: true, userToggleable: true, styleAvailable: true, + installedInBar: true, barWidget: true, enabled: true, group: "", + displayModes: ["full", "icon"], + replacementGroup: "", replacementTarget: "", + replacementTargetEnabled: false, replacementInEffect: false, + replacementLabel: "", replaced: false, replacedBy: "", + replacedByIds: [] + }]) + panel.v2LayoutActive = true + const v2BaseCount = appearance.activeWidgetCount + panel.thirdPartyAppearanceFixture = true + const thirdPartyCount = appearance.activeWidgetCount + const thirdPartyOpened = thirdPartyCount === v2BaseCount + 1 + && appearance.openWidgetDetails("G:example.chess", "") + const thirdPartyMode = appearance.selectedWidgetMode + const thirdPartyModeValues = appearance.selectedWidgetModeOptions + .map(function(option) { return String(option.value) }).join(",") + panel.v2LayoutActive = false + const thirdPartyV1Default = appearance.selectedWidgetModeOptions + .length === 1 + && appearance.selectedWidgetModeOptions[0].value === "full" + appearance.showWidgetOverview() + appearance.selectedWidgetGroup = "G4" + appearance.selectedWidgetId = "" + panel.pluginEntries = savedEntries.concat([{ + id: "example.plain", name: "Example Plain", description: "", + author: "Example", category: "Info", searchTags: [], + kinds: ["bar-widget"], glyph: "widgets", provider: "Third-party", + removable: true, userToggleable: true, styleAvailable: true, + installedInBar: true, barWidget: true, enabled: true, group: "", + displayModes: [], + replacementGroup: "", replacementTarget: "", + replacementTargetEnabled: false, replacementInEffect: false, + replacementLabel: "", replaced: false, replacedBy: "", + replacedByIds: [] + }]) + panel.v2LayoutActive = true + const undeclaredHidden = appearance.activeWidgetCount === v2BaseCount + panel.v2LayoutActive = false + panel.thirdPartyAppearanceFixture = false + panel.pluginEntries = savedEntries + if (!thirdPartyOpened) + return root.fail("declared third-party widget was not listed: " + + thirdPartyCount + " active widgets, expected " + + (v2BaseCount + 1)) + if (thirdPartyMode !== "full" || thirdPartyModeValues !== "icon,full") + return root.fail("third-party display modes were " + + thirdPartyModeValues + " with mode " + thirdPartyMode) + if (!thirdPartyV1Default) + return root.fail("third-party V1 appearance is not Default-only") + if (!undeclaredHidden) + return root.fail("undeclared third-party widget entered the Icons editor") + if (appearance.activeWidgetCount !== 12 + || appearance.inactiveWidgetCount !== 6) + return root.fail("third-party appearance fixture cleanup failed") if (!appearance.resetActionVisible || appearance.resetConfirmationPending || appearance.activeResetVariant !== "v1" diff --git a/tests/fixtures/ControlCenterTestPanel.qml b/tests/fixtures/ControlCenterTestPanel.qml index b8706d2..5397e27 100644 --- a/tests/fixtures/ControlCenterTestPanel.qml +++ b/tests/fixtures/ControlCenterTestPanel.qml @@ -73,20 +73,29 @@ Item { property string lastSwitchTarget: "" property string lastQuickSystemAction: "" property int reloadCalls: 0 - readonly property var activeWidgetOrder: v2LayoutActive - ? { - left: ["G1", "G2", "G3", "G5", "G6", "G4", "G7"], - center: ["G8"], - right: [ - "G9", "G10", "G11", "G14", "G12", "G13", "G16", - "G18", "G17", "G15" - ] - } - : { - left: ["G1", "G2", "G3", "G4", "G5", "G6", "G7"], - center: ["G8"], - right: ["G9", "G10", "G11", "G12", "G13", "G14", "G15"] - } + // Appends a third-party dynamic group to the active order so the smoke + // test can exercise x-shibumi.displayModes without changing the baseline + // widget counts every other phase asserts. + property bool thirdPartyAppearanceFixture: false + readonly property var activeWidgetOrder: { + const order = v2LayoutActive + ? { + left: ["G1", "G2", "G3", "G5", "G6", "G4", "G7"], + center: ["G8"], + right: [ + "G9", "G10", "G11", "G14", "G12", "G13", "G16", + "G18", "G17", "G15" + ] + } + : { + left: ["G1", "G2", "G3", "G4", "G5", "G6", "G7"], + center: ["G8"], + right: ["G9", "G10", "G11", "G12", "G13", "G14", "G15"] + } + if (thirdPartyAppearanceFixture) + order.right = order.right.concat(["G:example.chess"]) + return order + } readonly property bool quickNetworkAvailable: true readonly property bool quickNetworkEnabled: true readonly property string quickNetworkLabel: "Fixture Wi-Fi"