From 5691e26278e278141f682cc173a0e47eef10ad66 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 09:53:08 +0100 Subject: [PATCH 01/13] [ryoku] qsbar: add isolateMonitors toggle to workspaces catalogue --- ryoku/shell/ipc/bar_test.go | 27 +++++++++++++++++-- .../bar/barstyles/qsbar/core/widgets.json | 6 ++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/ryoku/shell/ipc/bar_test.go b/ryoku/shell/ipc/bar_test.go index 673a87e62..8c98c01cb 100644 --- a/ryoku/shell/ipc/bar_test.go +++ b/ryoku/shell/ipc/bar_test.go @@ -21,8 +21,9 @@ const catalogueFixture = `[ { "key": "launcherLogoIcon", "type": "choice", "label": "Glyph", "options": ["ryoku", "arch", "dragon"] } ] }, { "id": "workspaces", "gid": "G2", "label": "Workspaces", "gloss": "間", "category": "Bar", "settings": [ - { "key": "workspaceMode", "type": "choice", "label": "Shown", "options": ["active", "5", "10"] }, - { "key": "workspaceStyle", "type": "choice", "label": "Marker", "options": ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"] } ] }, + { "key": "workspaceMode", "type": "choice", "label": "Shown", "options": ["active", "5", "10"] }, + { "key": "workspaceStyle", "type": "choice", "label": "Marker", "options": ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"] }, + { "key": "isolateMonitors", "type": "toggle", "label": "Isolate monitors" } ] }, { "id": "status", "gid": "G3", "label": "Status", "visKey": "status", "desc": "Arch update, tray, notifications" }, { "id": "memory", "gid": "G4", "label": "Memory", "visKey": "memory" }, { "id": "cpu", "gid": "G5", "label": "CPU", "visKey": "cpu" }, @@ -447,6 +448,28 @@ func TestBarVerbsThroughStore(t *testing.T) { } } +// TestBarWorkspacesIsolateMonitors verifies that the isolateMonitors toggle +// is accepted by the bar-set verb and round-trips through the settings store. +func TestBarWorkspacesIsolateMonitors(t *testing.T) { + d := barTestDaemon(t) + + // Turn the toggle on. + if got := d.dispatch("bar set workspaces isolateMonitors on"); got != "ok" { + t.Fatalf("bar set isolateMonitors on: %s", got) + } + if qs, _ := d.frame(t)["qsbar"].(map[string]any); qs["isolateMonitors"] != true { + t.Fatalf("qsbar.isolateMonitors = %v, want true", qs["isolateMonitors"]) + } + + // Turn it back off. + if got := d.dispatch("bar set workspaces isolateMonitors off"); got != "ok" { + t.Fatalf("bar set isolateMonitors off: %s", got) + } + if qs, _ := d.frame(t)["qsbar"].(map[string]any); qs["isolateMonitors"] != false { + t.Fatalf("qsbar.isolateMonitors = %v, want false", qs["isolateMonitors"]) + } +} + // TestDockVerbsThroughStore drives the dock verbs and checks the top-level dock // object round-trips, keeping the shipped fields the CLI does not touch. func TestDockVerbsThroughStore(t *testing.T) { diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json index 8b2c0da93..9a0e5882a 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json @@ -2,7 +2,11 @@ { "id": "launcher", "gid": "G1", "label": "Launcher", "gloss": "起動", "category": "Bar", "desc": "The mark that opens the launcher and this panel. Choose it in Identity." }, { "id": "workspaces", "gid": "G2", "label": "Workspaces", "gloss": "間", "category": "Bar", - "desc": "The workspace row. Its count and marker are set in Identity." }, + "desc": "The workspace row. Its count and marker are set in Identity.", + "settings": [ + { "key": "workspaceMode", "type": "choice", "label": "Shown", "options": ["active", "5", "10"] }, + { "key": "workspaceStyle", "type": "choice", "label": "Marker", "options": ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"] }, + { "key": "isolateMonitors", "type": "toggle", "label": "Isolate monitors" } ] }, { "id": "status", "gid": "G3", "label": "Status", "visKey": "status", "desc": "Arch update, tray, notifications" }, { "id": "memory", "gid": "G4", "label": "Memory", "visKey": "memory" }, { "id": "cpu", "gid": "G5", "label": "CPU", "visKey": "cpu" }, From 6f868a30306f23bb0e7bfaf5f3a51a2c19836139 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 09:55:31 +0100 Subject: [PATCH 02/13] [ryoku] qsbar/Theme: add isolateMonitors property with persistence --- .../quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml index 3327509a5..1bc6803b0 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml @@ -2342,6 +2342,8 @@ Item { // ── workspace display style (orthogonal to mode; persisted) ── // "rings" is the stable cache token for the user-facing Frame style. property string workspaceStyle: "default" // default, numbers, magic, kanji, rings, aurora + // ── workspace monitor isolation (persisted) ── + property bool isolateMonitors: false // The marker styles this variant offers (continuous V2 adds Kanji, Frame, // Aurora; "rings" is the persisted cache token for the Frame style). readonly property var workspaceStyleOptions: [ @@ -2713,6 +2715,7 @@ Item { onLauncherLogoIconChanged: if (_widgetsLoaded) saveWidgets() onWeatherImperialChanged: if (_widgetsLoaded) saveWidgets() onWorkspaceStyleChanged: if (_widgetsLoaded) saveWidgets() + onIsolateMonitorsChanged: if (_widgetsLoaded) persistWidgetsToConfig() onBarPositionChanged: if (_widgetsLoaded) saveWidgets() onBarScaleChanged: if (_widgetsLoaded) saveWidgets() onBarShellStyleChanged: if (_widgetsLoaded) saveWidgets() @@ -3010,6 +3013,7 @@ Item { q.barPosition = barPosition q.workspaceMode = workspaceMode q.workspaceStyle = workspaceStyle + q.isolateMonitors = isolateMonitors q.pickerStyle = pickerStyle q.launcherLogoMode = launcherLogoMode q.aiTools = serializeAiTools() @@ -3079,6 +3083,7 @@ Item { if (q.barPosition === "top" || q.barPosition === "bottom") barPosition = q.barPosition if (q.workspaceMode !== undefined) workspaceMode = q.workspaceMode if (q.workspaceStyle !== undefined) workspaceStyle = q.workspaceStyle + if (q.isolateMonitors !== undefined) isolateMonitors = q.isolateMonitors === true if (q.pickerStyle !== undefined) pickerStyle = q.pickerStyle if (q.launcherLogoMode !== undefined) launcherLogoMode = q.launcherLogoMode if (q.aiTools !== undefined) parseAiTools(q.aiTools) From b354fa0552f4f98524c1b44c30248deafd95f21a Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 09:55:52 +0100 Subject: [PATCH 03/13] [ryoku] qsbar/BarSlot: thread screenName into WorkspaceWidget --- .../quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml index 2980bd309..c7ca75150 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml @@ -941,6 +941,7 @@ PanelWindow { id: compWorkspace WorkspaceWidget { root: barSlot.root + screenName: barSlot.screenName readonly property real barContentLeftInset: 0 readonly property real barContentRightInset: 0 } From fc60a1e84441bd430013653cdb01d2602f0bdef3 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 09:58:42 +0100 Subject: [PATCH 04/13] [ryoku] qsbar/WorkspaceWidget: per-monitor filtering and focus dimming --- .../qsbar/modules/WorkspaceWidget.qml | 79 ++++++++++++++++--- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml index 0f1d380ee..f631db98c 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml @@ -6,6 +6,7 @@ import shell.services Item { id: wsWidget required property var root + property string screenName: "" // Workspace cells intentionally stay dense without a local surface. A widget // border needs its own breathing room, though: make that padding part of the @@ -16,6 +17,24 @@ Item { implicitHeight: 28 readonly property color contentColor: root.widgetContentColor("G2", root.seal) + // Per-monitor active workspace: when isolateMonitors is active, this monitor's + // active workspace takes precedence over the globally focused one. + readonly property var monitorActiveWs: { + if (!root.isolateMonitors || !wsWidget.screenName) return Wm.focusedWorkspace + var ws = Wm.workspaces + for (var i = 0; i < ws.length; i++) { + if (!ws[i].special && ws[i].active && ws[i].output === wsWidget.screenName) + return ws[i] + } + return Wm.focusedWorkspace + } + + // Indicates whether this monitor holds the keyboard/window input focus. + readonly property bool hasMonitorFocus: !root.isolateMonitors + || wsWidget.screenName === "" + || Wm.focusedOutput === "" + || Wm.focusedOutput === wsWidget.screenName + // The focused workspace name only when it is a real workspace beyond the // persist range, else "". Renotifies on value change only, so switching // between in-range workspaces keeps the model stable and the delegate @@ -24,7 +43,7 @@ Item { readonly property string extraWs: { if (root.workspaceMode === "active" || wsWidget.dynamicModel) return "" var n = root.workspaceMode === "5" ? 5 : 10 - var f = Wm.focusedWorkspace + var f = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace var num = f ? Number(f.name) : 0 return (num > n) ? f.name : "" } @@ -35,18 +54,41 @@ Item { if (wsWidget.dynamicModel) { var live = [] var lw = Wm.workspaces - for (var k = 0; k < lw.length; k++) if (!lw[k].special) live.push(lw[k].name) + for (var k = 0; k < lw.length; k++) { + if (lw[k].special) continue + if (root.isolateMonitors && wsWidget.screenName && lw[k].output && lw[k].output !== wsWidget.screenName) + continue + live.push(lw[k].name) + } return live } if (root.workspaceMode === "active") { var ids = {} var ws = Wm.workspaces - for (var i = 0; i < ws.length; i++) if (!ws[i].special) ids[ws[i].name] = true - if (Wm.focusedWorkspace && !Wm.focusedWorkspace.special) ids[Wm.focusedWorkspace.name] = true + for (var i = 0; i < ws.length; i++) { + if (ws[i].special) continue + if (root.isolateMonitors && wsWidget.screenName && ws[i].output && ws[i].output !== wsWidget.screenName) + continue + ids[ws[i].name] = true + } + var act = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace + if (act && !act.special) { + if (!root.isolateMonitors || !wsWidget.screenName || !act.output || act.output === wsWidget.screenName) + ids[act.name] = true + } return Object.keys(ids).sort(function(a, b) { return Number(a) - Number(b) }) } var n = root.workspaceMode === "5" ? 5 : 10 - var list = []; for (var j = 1; j <= n; j++) list.push(String(j)) + var list = [] + for (var j = 1; j <= n; j++) { + var wName = String(j) + if (root.isolateMonitors && wsWidget.screenName) { + var existing = Wm.workspaceByName(wName) + if (existing && existing.output && existing.output !== wsWidget.screenName) + continue + } + list.push(wName) + } if (extraWs !== "") list.push(extraWs) return list } @@ -55,8 +97,10 @@ Item { // mark occupied cells, dimmed dots the empty ones. On a focus change a // runner pacman travels from the old cell to the new one, chomping as it // goes; the destination pellet is eaten (fades/shrinks) as it arrives. ── - readonly property string focusedWorkspaceKey: Wm.focusedWorkspace - && !Wm.focusedWorkspace.special ? Wm.focusedWorkspace.name : "" + readonly property string focusedWorkspaceKey: { + var fw = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace + return fw && !fw.special ? fw.name : "" + } property string pacmanLastFocusedWorkspaceKey: "" property string pacmanTargetWorkspaceKey: "" @@ -199,8 +243,10 @@ Item { // default-only `dot`, invisible in numbers/magic) Behavior on scale { NumberAnimation { duration: 120 } } - readonly property bool isFocused: Wm.focusedWorkspace !== null - && Wm.focusedWorkspace.name === wsKey + readonly property bool isFocused: { + var act = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace + return act !== null && act.name === wsKey + } readonly property bool isOccupied: { var w = Wm.workspaceByName(wsKey) @@ -222,7 +268,13 @@ Item { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } - // ── DEFAULT style: glow + dot ── + Item { + id: wsCellVisual + anchors.fill: parent + opacity: wsCell.isFocused && !wsWidget.hasMonitorFocus ? 0.72 : 1.0 + Behavior on opacity { NumberAnimation { duration: 150 } } + + // ── DEFAULT style: glow + dot ── // glow - alle states, nur opacity variiert Rectangle { visible: root.workspaceStyle === "default" @@ -405,6 +457,7 @@ Item { emptyColor: wsWidget.contentColor hoverColor: wsWidget.contentColor } + } MouseArea { id: wsMa @@ -429,9 +482,11 @@ Item { anchors.fill: parent z: 0 visible: root.workspaceStyle === "rings" && targetIndex >= 0 + opacity: !wsWidget.hasMonitorFocus ? 0.72 : 1.0 + Behavior on opacity { NumberAnimation { duration: 150 } } readonly property int targetIndex: { - var focused = Wm.focusedWorkspace + var focused = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace if (!focused || focused.special) return -1 return wsWidget.workspaceList.indexOf(focused.name) } @@ -500,6 +555,8 @@ Item { Item { id: pacmanRunner visible: wsWidget.pacmanTraveling && root.workspaceStyle === "pacman" + opacity: !wsWidget.hasMonitorFocus ? 0.72 : 1.0 + Behavior on opacity { NumberAnimation { duration: 150 } } z: 4 x: wsWidget.pacmanTravelX - width / 2 y: Math.round((parent.height - height) / 2) From 7744fcd62debb2c56c110c81988b3a993d453cb3 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 09:59:30 +0100 Subject: [PATCH 05/13] [ryoku] qsbar/WorkspacePanel: filter popup by monitor when isolateMonitors --- .../barstyles/qsbar/panels/WorkspacePanel.qml | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml index ab296ab9f..05de3f8df 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml @@ -34,11 +34,22 @@ PanelWindow { Rectangle { id: card property int navIndex: -1 + readonly property var filteredWorkspaces: { + var res = [] + var vs = Wm.workspaces + for (var i = 0; i < vs.length; i++) { + if (vs[i].special) continue + if (root.isolateMonitors && root.activePopupScreenName && vs[i].output && vs[i].output !== root.activePopupScreenName) + continue + res.push(vs[i]) + } + res.sort(function(x, y) { return Number(x.name) - Number(y.name) }) + return res + } readonly property var wsIds: { var a = [] - var vs = Wm.workspaces - for (var i = 0; i < vs.length; i++) if (!vs[i].special) a.push(vs[i].name) - a.sort(function(x, y) { return Number(x) - Number(y) }) + var vs = card.filteredWorkspaces + for (var i = 0; i < vs.length; i++) a.push(vs[i].name) return a } width: 240 @@ -102,12 +113,20 @@ PanelWindow { width: parent.width spacing: 4 Repeater { - model: Wm.workspaces + model: card.filteredWorkspaces delegate: Rectangle { required property var modelData - visible: !modelData.special - readonly property bool isActive: Wm.focusedWorkspace && Wm.focusedWorkspace.name === modelData.name + readonly property bool isActive: { + if (root.isolateMonitors && root.activePopupScreenName) { + var ws = Wm.workspaces + for (var i = 0; i < ws.length; i++) { + if (!ws[i].special && ws[i].active && ws[i].output === root.activePopupScreenName) + return ws[i].name === modelData.name + } + } + return Wm.focusedWorkspace && Wm.focusedWorkspace.name === modelData.name + } readonly property bool navOn: card.navIndex >= 0 && card.navIndex < card.wsIds.length && card.wsIds[card.navIndex] === modelData.name width: col.width height: 30; radius: root.panelButtonRadius From 4818296c7544dee4eb87b30916af4855b8df1a02 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 10:10:07 +0100 Subject: [PATCH 06/13] [ryoku] qsbar/IdentityRoute: add isolateMonitors toggle in Workspaces section --- .../controlcenter/routes/IdentityRoute.qml | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml index dc005ac8e..d742f9571 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml @@ -304,10 +304,37 @@ Item { } - // ── PREVIEW ── + // ── workspaces: ISOLATE ── Entrance { width: page.colW index: 4 + SettingCard { + width: page.colW + title: I18n.tr("ISOLATE") + kana: "\u5206" + + SettingRow { + anchors.left: parent.left + anchors.right: parent.right + label: I18n.tr("Per-monitor") + desc: I18n.tr("Each bar shows only the workspaces on its own screen.") + source: "shell.json" + controlWidth: Math.max(120, 62 * 2) + Seg { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + options: [I18n.tr("Off"), I18n.tr("On")] + current: page.root && page.root.isolateMonitors ? I18n.tr("On") : I18n.tr("Off") + onChose: (key) => { if (page.root) page.root.isolateMonitors = (key === I18n.tr("On")) } + } + } + } + } + + // ── PREVIEW ── + Entrance { + width: page.colW + index: 5 SettingCard { width: page.colW title: I18n.tr("PREVIEW") From 8b1c349b31b96f2e6bf5102a41ecf3e096191279 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 10:37:22 +0100 Subject: [PATCH 07/13] [docs] changelog: document qsbar isolateMonitors (#767) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 099cd8352..b96ce49ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ for finer detail. ## Unreleased +### Added +- QS Bar adds per-monitor workspace isolation (`isolateMonitors`), allowing + multi-monitor setups to show only the workspaces assigned to each screen on its + respective bar. The inactive screen dims its active workspace indicator to 72% + opacity to convey keyboard focus, and the right-click WorkspacePanel popup also + filters by screen. Accessible via `shell.json` (`qsbar.isolateMonitors`), + `ryoku-shell bar set workspaces isolateMonitors on/off`, and QS Bar Settings > + Identity > Workspaces (#767). + ### Removed - The mango compositor is gone: its provider, config tree, packages, installer choice, deploy steps, tests and docs. Ryoku ships Hyprland and From 1776900f3b0cfbcdd4e23a1181f1bcdfb5bba825 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 11:30:54 +0100 Subject: [PATCH 08/13] [ryoku] qsbar: address review feedback on workspace isolation (#767) --- ryoku/i18n/catalog/en.json | 2 ++ .../bar/barstyles/qsbar/modules/WorkspaceWidget.qml | 2 +- .../modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ryoku/i18n/catalog/en.json b/ryoku/i18n/catalog/en.json index 926a0e4c0..d97c5d24c 100644 --- a/ryoku/i18n/catalog/en.json +++ b/ryoku/i18n/catalog/en.json @@ -3465,6 +3465,7 @@ "EXTRACTING": "EXTRACTING", "Each add-on defines its own; applied live": "Each add-on defines its own; applied live", "Each app's level under the sound card.": "Each app's level under the sound card.", + "Each bar shows only the workspaces on its own screen.": "Each bar shows only the workspaces on its own screen.", "Each combo your config wants that Ryoku already uses. Keep Ryoku's shortcut, use yours (Ryoku unbinds its own so yours actually wins), or remap yours to a free chord. Untouched rows keep Ryoku's.": "Each combo your config wants that Ryoku already uses. Keep Ryoku's shortcut, use yours (Ryoku unbinds its own so yours actually wins), or remap yours to a free chord. Untouched rows keep Ryoku's.", "Each device keeps its own bindings; unplug it and nothing else changes.": "Each device keeps its own bindings; unplug it and nothing else changes.", "Each display keeps its own placement and audio settings.": "Each display keeps its own placement and audio settings.", @@ -7288,6 +7289,7 @@ "Per surface": "Per surface", "Per-animation control, for when a feel isn't enough. Each row is one desktop animation: turn it off, change its speed, its curve, or its style.": "Per-animation control, for when a feel isn't enough. Each row is one desktop animation: turn it off, change its speed, its curve, or its style.", "Per-app look on top of the global Appearance": "Per-app look on top of the global Appearance", + "Per-monitor": "Per-monitor", "Per-widget colour": "Per-widget colour", "Percentage from the system battery. Unknown battery state fails closed.": "Percentage from the system battery. Unknown battery state fails closed.", "Percentage meaning": "Percentage meaning", diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml index f631db98c..457210eb9 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml @@ -26,7 +26,7 @@ Item { if (!ws[i].special && ws[i].active && ws[i].output === wsWidget.screenName) return ws[i] } - return Wm.focusedWorkspace + return null } // Indicates whether this monitor holds the keyboard/window input focus. diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml index 05de3f8df..abc41144c 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml @@ -43,13 +43,18 @@ PanelWindow { continue res.push(vs[i]) } - res.sort(function(x, y) { return Number(x.name) - Number(y.name) }) + if (root.isolateMonitors) { + res.sort(function(x, y) { return Number(x.name) - Number(y.name) }) + } return res } readonly property var wsIds: { var a = [] var vs = card.filteredWorkspaces for (var i = 0; i < vs.length; i++) a.push(vs[i].name) + if (!root.isolateMonitors) { + a.sort(function(x, y) { return Number(x) - Number(y) }) + } return a } width: 240 From b753286d3c7d800f33cd59644bfcd992152da5b8 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 11:36:45 +0100 Subject: [PATCH 09/13] [ryoku] qsbar: fix wsIds/filteredWorkspaces order mismatch in WorkspacePanel Always sort filteredWorkspaces numerically (not only when isolateMonitors is on) so the repeater and wsIds are always in the same order. wsIds is now derived directly from filteredWorkspaces without an independent sort, which fixes keyboard navigation when the compositor returns workspaces out of numeric order and isolation is off. Addresses mika review finding on PR #770. --- .../bar/barstyles/qsbar/panels/WorkspacePanel.qml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml index abc41144c..6052bdf49 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml @@ -43,18 +43,13 @@ PanelWindow { continue res.push(vs[i]) } - if (root.isolateMonitors) { - res.sort(function(x, y) { return Number(x.name) - Number(y.name) }) - } + res.sort(function(x, y) { return Number(x.name) - Number(y.name) }) return res } readonly property var wsIds: { - var a = [] var vs = card.filteredWorkspaces + var a = [] for (var i = 0; i < vs.length; i++) a.push(vs[i].name) - if (!root.isolateMonitors) { - a.sort(function(x, y) { return Number(x) - Number(y) }) - } return a } width: 240 From 538c985d42499d1e48e24cfc80fea0f6e92aab3d Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 11:51:20 +0100 Subject: [PATCH 10/13] [ryoku] qsbar: cover per-monitor workspace isolation --- .../qsbar/modules/WorkspaceIsolation.js | 63 +++++++++++++++ .../qsbar/modules/WorkspaceIsolation.test.mjs | 36 +++++++++ .../qsbar/modules/WorkspaceWidget.qml | 80 +++---------------- 3 files changed, 110 insertions(+), 69 deletions(-) create mode 100644 ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js create mode 100644 ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js new file mode 100644 index 000000000..bba12808f --- /dev/null +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js @@ -0,0 +1,63 @@ +.pragma library + +function activeWorkspace(workspaces, screenName, isolateMonitors, focusedWorkspace) { + if (!isolateMonitors || !screenName) return focusedWorkspace + for (var i = 0; i < workspaces.length; i++) { + var workspace = workspaces[i] + if (!workspace.special && workspace.active && workspace.output === screenName) + return workspace + } + return null +} + +function workspaceList(workspaces, mode, dynamicModel, screenName, isolateMonitors, active) { + if (dynamicModel) { + var live = [] + for (var i = 0; i < workspaces.length; i++) { + var workspace = workspaces[i] + if (workspace.special) continue + if (isolateMonitors && screenName && workspace.output && workspace.output !== screenName) + continue + live.push(workspace.name) + } + return live + } + + if (mode === "active") { + var ids = {} + for (var j = 0; j < workspaces.length; j++) { + var current = workspaces[j] + if (current.special) continue + if (isolateMonitors && screenName && current.output && current.output !== screenName) + continue + ids[current.name] = true + } + if (active && !active.special + && (!isolateMonitors || !screenName || !active.output || active.output === screenName)) + ids[active.name] = true + return Object.keys(ids).sort(function(a, b) { return Number(a) - Number(b) }) + } + + var count = mode === "5" ? 5 : 10 + var fixed = [] + for (var k = 1; k <= count; k++) { + var name = String(k) + var existing = workspaces.find(function(workspace) { return workspace.name === name }) + if (isolateMonitors && screenName && existing && existing.output && existing.output !== screenName) + continue + fixed.push(name) + } + if (active) { + var activeNumber = Number(active.name) + if (activeNumber > count) fixed.push(active.name) + } + return fixed +} + +function hasMonitorFocus(isolateMonitors, screenName, focusedOutput) { + return !isolateMonitors || !screenName || !focusedOutput || focusedOutput === screenName +} + +function markerOpacity(hasFocus) { + return hasFocus ? 1.0 : 0.72 +} diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs new file mode 100644 index 000000000..f65ed826d --- /dev/null +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import vm from "node:vm"; + +const source = await readFile(new URL("./WorkspaceIsolation.js", import.meta.url), "utf8"); +const module = { exports: {} }; +vm.runInNewContext(source.replace(/^\.pragma library\s*/, "") + + "\nmodule.exports = { activeWorkspace, workspaceList, hasMonitorFocus, markerOpacity };", + { module, Number, Object }); +const { activeWorkspace, workspaceList, hasMonitorFocus, markerOpacity } = module.exports; + +const workspaces = [ + { name: "1", output: "eDP-1", active: true, special: false }, + { name: "2", output: "DP-1", active: false, special: false }, + { name: "12", output: "DP-1", active: true, special: false }, + { name: "special:1", output: "eDP-1", active: false, special: true } +]; + +assert.equal(activeWorkspace(workspaces, "eDP-1", true, workspaces[2]), workspaces[0], + "each output selects its own active workspace"); +assert.equal(activeWorkspace(workspaces, "DP-1", true, workspaces[0]), workspaces[2], + "the other output selects its own active workspace"); +assert.equal(activeWorkspace(workspaces, "eDP-1", false, workspaces[2]), workspaces[2], + "disabling isolation keeps the globally focused workspace"); + +assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "eDP-1", true, workspaces[0])), + ["1", "3", "4", "5"], "fixed mode filters assigned workspaces and keeps uncreated slots"); +assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "DP-1", true, workspaces[2])), + ["2", "3", "4", "5", "12"], "fixed mode includes the local active workspace beyond its range"); + +let focusedOutput = "DP-1"; +assert.equal(markerOpacity(hasMonitorFocus(true, "eDP-1", focusedOutput)), 0.72, + "an active marker dims when another output has focus"); +focusedOutput = "eDP-1"; +assert.equal(markerOpacity(hasMonitorFocus(true, "eDP-1", focusedOutput)), 1.0, + "the active marker brightens when focus moves to its output"); diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml index 457210eb9..6063f22c6 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml @@ -2,6 +2,7 @@ import Ryoku.Ui.Singletons import QtQuick import QtQuick.Shapes import shell.services +import "WorkspaceIsolation.js" as WorkspaceIsolation Item { id: wsWidget @@ -19,79 +20,20 @@ Item { // Per-monitor active workspace: when isolateMonitors is active, this monitor's // active workspace takes precedence over the globally focused one. - readonly property var monitorActiveWs: { - if (!root.isolateMonitors || !wsWidget.screenName) return Wm.focusedWorkspace - var ws = Wm.workspaces - for (var i = 0; i < ws.length; i++) { - if (!ws[i].special && ws[i].active && ws[i].output === wsWidget.screenName) - return ws[i] - } - return null - } + readonly property var monitorActiveWs: WorkspaceIsolation.activeWorkspace( + Wm.workspaces, wsWidget.screenName, root.isolateMonitors, Wm.focusedWorkspace) // Indicates whether this monitor holds the keyboard/window input focus. - readonly property bool hasMonitorFocus: !root.isolateMonitors - || wsWidget.screenName === "" - || Wm.focusedOutput === "" - || Wm.focusedOutput === wsWidget.screenName - - // The focused workspace name only when it is a real workspace beyond the - // persist range, else "". Renotifies on value change only, so switching - // between in-range workspaces keeps the model stable and the delegate - // Behaviors animating. + readonly property bool hasMonitorFocus: WorkspaceIsolation.hasMonitorFocus( + root.isolateMonitors, wsWidget.screenName, Wm.focusedOutput) + readonly property bool dynamicModel: Wm.workspaceModel === "dynamic" - readonly property string extraWs: { - if (root.workspaceMode === "active" || wsWidget.dynamicModel) return "" - var n = root.workspaceMode === "5" ? 5 : 10 - var f = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace - var num = f ? Number(f.name) : 0 - return (num > n) ? f.name : "" - } // A dynamic model follows the live workspace list with no invented slots; a // fixed model shows a stable numbered row plus the focused-beyond-range one. - readonly property var workspaceList: { - if (wsWidget.dynamicModel) { - var live = [] - var lw = Wm.workspaces - for (var k = 0; k < lw.length; k++) { - if (lw[k].special) continue - if (root.isolateMonitors && wsWidget.screenName && lw[k].output && lw[k].output !== wsWidget.screenName) - continue - live.push(lw[k].name) - } - return live - } - if (root.workspaceMode === "active") { - var ids = {} - var ws = Wm.workspaces - for (var i = 0; i < ws.length; i++) { - if (ws[i].special) continue - if (root.isolateMonitors && wsWidget.screenName && ws[i].output && ws[i].output !== wsWidget.screenName) - continue - ids[ws[i].name] = true - } - var act = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace - if (act && !act.special) { - if (!root.isolateMonitors || !wsWidget.screenName || !act.output || act.output === wsWidget.screenName) - ids[act.name] = true - } - return Object.keys(ids).sort(function(a, b) { return Number(a) - Number(b) }) - } - var n = root.workspaceMode === "5" ? 5 : 10 - var list = [] - for (var j = 1; j <= n; j++) { - var wName = String(j) - if (root.isolateMonitors && wsWidget.screenName) { - var existing = Wm.workspaceByName(wName) - if (existing && existing.output && existing.output !== wsWidget.screenName) - continue - } - list.push(wName) - } - if (extraWs !== "") list.push(extraWs) - return list - } + readonly property var workspaceList: WorkspaceIsolation.workspaceList( + Wm.workspaces, root.workspaceMode, wsWidget.dynamicModel, wsWidget.screenName, + root.isolateMonitors, root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace) // ── PACMAN style state: pacman rests on the focused cell, pellet glyphs // mark occupied cells, dimmed dots the empty ones. On a focus change a @@ -271,7 +213,7 @@ Item { Item { id: wsCellVisual anchors.fill: parent - opacity: wsCell.isFocused && !wsWidget.hasMonitorFocus ? 0.72 : 1.0 + opacity: wsCell.isFocused ? WorkspaceIsolation.markerOpacity(wsWidget.hasMonitorFocus) : 1.0 Behavior on opacity { NumberAnimation { duration: 150 } } // ── DEFAULT style: glow + dot ── @@ -482,7 +424,7 @@ Item { anchors.fill: parent z: 0 visible: root.workspaceStyle === "rings" && targetIndex >= 0 - opacity: !wsWidget.hasMonitorFocus ? 0.72 : 1.0 + opacity: WorkspaceIsolation.markerOpacity(wsWidget.hasMonitorFocus) Behavior on opacity { NumberAnimation { duration: 150 } } readonly property int targetIndex: { From 3ce7431e18a26569f8b9e147f599d41b4be7394b Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 12:18:37 +0100 Subject: [PATCH 11/13] [ryoku] qsbar: keep fixed workspace model stable --- .../barstyles/qsbar/modules/WorkspaceIsolation.js | 13 ++++++++----- .../qsbar/modules/WorkspaceIsolation.test.mjs | 13 +++++++++---- .../bar/barstyles/qsbar/modules/WorkspaceWidget.qml | 9 ++++++++- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js index bba12808f..5e26dd345 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js @@ -10,7 +10,7 @@ function activeWorkspace(workspaces, screenName, isolateMonitors, focusedWorkspa return null } -function workspaceList(workspaces, mode, dynamicModel, screenName, isolateMonitors, active) { +function workspaceList(workspaces, mode, dynamicModel, screenName, isolateMonitors, active, extraWorkspace) { if (dynamicModel) { var live = [] for (var i = 0; i < workspaces.length; i++) { @@ -47,13 +47,16 @@ function workspaceList(workspaces, mode, dynamicModel, screenName, isolateMonito continue fixed.push(name) } - if (active) { - var activeNumber = Number(active.name) - if (activeNumber > count) fixed.push(active.name) - } + if (extraWorkspace) fixed.push(extraWorkspace) return fixed } +function extraWorkspace(focused, mode) { + var count = mode === "5" ? 5 : 10 + var number = focused ? Number(focused.name) : 0 + return number > count ? focused.name : "" +} + function hasMonitorFocus(isolateMonitors, screenName, focusedOutput) { return !isolateMonitors || !screenName || !focusedOutput || focusedOutput === screenName } diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs index f65ed826d..9ceafc95e 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs @@ -5,9 +5,9 @@ import vm from "node:vm"; const source = await readFile(new URL("./WorkspaceIsolation.js", import.meta.url), "utf8"); const module = { exports: {} }; vm.runInNewContext(source.replace(/^\.pragma library\s*/, "") + - "\nmodule.exports = { activeWorkspace, workspaceList, hasMonitorFocus, markerOpacity };", + "\nmodule.exports = { activeWorkspace, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity };", { module, Number, Object }); -const { activeWorkspace, workspaceList, hasMonitorFocus, markerOpacity } = module.exports; +const { activeWorkspace, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity } = module.exports; const workspaces = [ { name: "1", output: "eDP-1", active: true, special: false }, @@ -23,10 +23,15 @@ assert.equal(activeWorkspace(workspaces, "DP-1", true, workspaces[0]), workspace assert.equal(activeWorkspace(workspaces, "eDP-1", false, workspaces[2]), workspaces[2], "disabling isolation keeps the globally focused workspace"); -assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "eDP-1", true, workspaces[0])), +assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "eDP-1", true, null, "")), ["1", "3", "4", "5"], "fixed mode filters assigned workspaces and keeps uncreated slots"); -assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "DP-1", true, workspaces[2])), +assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "DP-1", true, null, "12")), ["2", "3", "4", "5", "12"], "fixed mode includes the local active workspace beyond its range"); +assert.equal(extraWorkspace({ name: "1" }, "5"), ""); +assert.equal(extraWorkspace({ name: "3" }, "5"), "", + "in-range focus changes keep the extra workspace binding unchanged"); +assert.equal(extraWorkspace({ name: "12" }, "5"), "12", + "fixed mode adds a workspace beyond its numbered range"); let focusedOutput = "DP-1"; assert.equal(markerOpacity(hasMonitorFocus(true, "eDP-1", focusedOutput)), 0.72, diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml index 6063f22c6..f830d913f 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml @@ -28,12 +28,19 @@ Item { root.isolateMonitors, wsWidget.screenName, Wm.focusedOutput) readonly property bool dynamicModel: Wm.workspaceModel === "dynamic" + readonly property var listActiveWorkspace: root.workspaceMode === "active" + ? (root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace) : null + readonly property string extraWs: { + if (root.workspaceMode === "active" || wsWidget.dynamicModel) return "" + var focused = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace + return WorkspaceIsolation.extraWorkspace(focused, root.workspaceMode) + } // A dynamic model follows the live workspace list with no invented slots; a // fixed model shows a stable numbered row plus the focused-beyond-range one. readonly property var workspaceList: WorkspaceIsolation.workspaceList( Wm.workspaces, root.workspaceMode, wsWidget.dynamicModel, wsWidget.screenName, - root.isolateMonitors, root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace) + root.isolateMonitors, wsWidget.listActiveWorkspace, wsWidget.extraWs) // ── PACMAN style state: pacman rests on the focused cell, pellet glyphs // mark occupied cells, dimmed dots the empty ones. On a focus change a From fa291b44afe77d9f1febf09a3a677f6b9a1b1731 Mon Sep 17 00:00:00 2001 From: san Date: Mon, 9 Nov 2026 18:32:49 +0100 Subject: [PATCH 12/13] [ryoku] qsbar: avoid live workspace model resets --- .../barstyles/qsbar/modules/WorkspaceIsolation.js | 4 ++++ .../qsbar/modules/WorkspaceIsolation.test.mjs | 13 +++++++++++-- .../bar/barstyles/qsbar/modules/WorkspaceWidget.qml | 4 +++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js index 5e26dd345..84d0048cc 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js @@ -10,6 +10,10 @@ function activeWorkspace(workspaces, screenName, isolateMonitors, focusedWorkspa return null } +function needsWorkspaceData(mode, dynamicModel, isolateMonitors) { + return dynamicModel || mode === "active" || isolateMonitors +} + function workspaceList(workspaces, mode, dynamicModel, screenName, isolateMonitors, active, extraWorkspace) { if (dynamicModel) { var live = [] diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs index 9ceafc95e..55215cc42 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs @@ -5,9 +5,9 @@ import vm from "node:vm"; const source = await readFile(new URL("./WorkspaceIsolation.js", import.meta.url), "utf8"); const module = { exports: {} }; vm.runInNewContext(source.replace(/^\.pragma library\s*/, "") + - "\nmodule.exports = { activeWorkspace, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity };", + "\nmodule.exports = { activeWorkspace, needsWorkspaceData, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity };", { module, Number, Object }); -const { activeWorkspace, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity } = module.exports; +const { activeWorkspace, needsWorkspaceData, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity } = module.exports; const workspaces = [ { name: "1", output: "eDP-1", active: true, special: false }, @@ -23,6 +23,15 @@ assert.equal(activeWorkspace(workspaces, "DP-1", true, workspaces[0]), workspace assert.equal(activeWorkspace(workspaces, "eDP-1", false, workspaces[2]), workspaces[2], "disabling isolation keeps the globally focused workspace"); +assert.equal(needsWorkspaceData("5", false, false), false, + "fixed mode without isolation does not track live workspace data"); +assert.equal(needsWorkspaceData("5", false, true), true, + "fixed mode needs live workspace data to filter monitor assignments"); +assert.equal(needsWorkspaceData("active", false, false), true, + "active mode needs live workspace data"); +assert.equal(needsWorkspaceData("5", true, false), true, + "dynamic mode needs live workspace data"); + assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "eDP-1", true, null, "")), ["1", "3", "4", "5"], "fixed mode filters assigned workspaces and keeps uncreated slots"); assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "DP-1", true, null, "12")), diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml index f830d913f..a78b404af 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml @@ -28,6 +28,8 @@ Item { root.isolateMonitors, wsWidget.screenName, Wm.focusedOutput) readonly property bool dynamicModel: Wm.workspaceModel === "dynamic" + readonly property var workspaceData: WorkspaceIsolation.needsWorkspaceData( + root.workspaceMode, wsWidget.dynamicModel, root.isolateMonitors) ? Wm.workspaces : [] readonly property var listActiveWorkspace: root.workspaceMode === "active" ? (root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace) : null readonly property string extraWs: { @@ -39,7 +41,7 @@ Item { // A dynamic model follows the live workspace list with no invented slots; a // fixed model shows a stable numbered row plus the focused-beyond-range one. readonly property var workspaceList: WorkspaceIsolation.workspaceList( - Wm.workspaces, root.workspaceMode, wsWidget.dynamicModel, wsWidget.screenName, + wsWidget.workspaceData, root.workspaceMode, wsWidget.dynamicModel, wsWidget.screenName, root.isolateMonitors, wsWidget.listActiveWorkspace, wsWidget.extraWs) // ── PACMAN style state: pacman rests on the focused cell, pellet glyphs From b413662b4f6c64a75dbeb79f9d4cb2c7c6c523d1 Mon Sep 17 00:00:00 2001 From: Carlos <90535780+neur0map@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:06:36 -0500 Subject: [PATCH 13/13] Update Theme.qml --- .../quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml index 1bc6803b0..12821569c 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml @@ -2342,7 +2342,7 @@ Item { // ── workspace display style (orthogonal to mode; persisted) ── // "rings" is the stable cache token for the user-facing Frame style. property string workspaceStyle: "default" // default, numbers, magic, kanji, rings, aurora - // ── workspace monitor isolation (persisted) ── +// Workspace monitor isolation (persisted) property bool isolateMonitors: false // The marker styles this variant offers (continuous V2 adds Kanji, Frame, // Aurora; "rings" is the persisted cache token for the Frame style).