Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
7 changes: 5 additions & 2 deletions docs/control-center-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions docs/plugin-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions hancore.shibumi.control-center/ControlCenterPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -488,6 +501,7 @@ ShibumiPanel {
version: String(manifest.version || ""),
kinds: kinds,
glyph: pluginGlyph(id, kinds),
displayModes: pluginDisplayModes(shibumi),
enabled: enabled,
barWidget: barWidget,
installedInBar: barWidget
Expand Down
48 changes: 45 additions & 3 deletions hancore.shibumi.control-center/WidgetAppearanceWorkbench.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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:<plugin id>" 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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 || ""))
Expand Down Expand Up @@ -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 ? [
Expand Down
4 changes: 4 additions & 0 deletions tests/control-center-regression.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)' \
Expand Down
64 changes: 64 additions & 0 deletions tests/control-center-smoke.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 23 additions & 14 deletions tests/fixtures/ControlCenterTestPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down