Skip to content

[ryoku] qsbar: per-monitor workspace isolation (fixes #767) - #770

Open
Satirito wants to merge 13 commits into
Ryoku-dev:unstable-devfrom
Satirito:feat/qsbar-workspace-isolate-monitors
Open

Satirito wants to merge 13 commits into
Ryoku-dev:unstable-devfrom
Satirito:feat/qsbar-workspace-isolate-monitors

Conversation

@Satirito

@Satirito Satirito commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

What changed

Implements per-monitor workspace isolation (isolateMonitors) for qsbar workspaces, addressing #767.

  • Added isolateMonitors boolean setting (default false) under qsbar.isolateMonitors in shell.json.
  • Exposed in catalogue widgets.json under workspaces, enabling CLI control via ryoku-shell bar set workspaces isolateMonitors on/off.
  • Added UI toggle in QS Bar Settings > Identity under Workspaces.
  • In multi-monitor setups with isolateMonitors enabled:
    • Each bar displays only the workspaces assigned to its monitor across dynamic, active, and fixed (1-5, 1-10) modes.
    • Each monitor highlights its own active workspace; the active marker dims to 72% opacity when the screen lacks keyboard focus.
  • The right-click WorkspacePanel popup also filters by screen when enabled.
  • Single-monitor setups and isolateMonitors: false retain 100% identical behavior.

Fixes #767

Area

ryoku

How it was tested

  • Added Go unit tests in ryoku/shell/ipc/bar_test.go (TestBarWorkspacesIsolateMonitors) verifying CLI toggling and round-tripping through the settings store (go test ./... in shell/ipc).
  • Validated all modified QML files (Theme.qml, BarSlot.qml, WorkspaceWidget.qml, WorkspacePanel.qml, IdentityRoute.qml) with qmllint (0 errors).

Checklist

  • One logical change, with a clear [area] scope: summary commit subject.
  • Matching CHANGELOG.md updated in the area I touched.
  • The git hooks pass locally; I did not use --no-verify.
  • Lua parses (luac -p), shell scripts pass bash -n, and QML passes qmllint where applicable.
  • No duplicated config, no dead code, no commented-out code, no em-dash.
  • Docs updated if behavior or layout changed.

@neur0map

neur0map commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

🦊 mika's review

Incremental review: new commits since the last checkpoint.

Summary

  • Add optional per-monitor workspace isolation to QS Bar.
  • Show local active workspaces and dim unfocused monitor markers.
  • Expose the setting through Identity, CLI, and persistent configuration.
  • Add workspace isolation and CLI round-trip tests.

Change diagram

flowchart LR
  Settings["Identity settings / CLI"] --> Theme["Persisted QS Bar setting"]
  WM["WM workspace and output state"] --> Widget["Workspace widget"]
  WM --> Panel["Workspace popup"]
  Theme --> Widget
  Theme --> Panel
  Helper["Workspace isolation helper"] --> Widget
Loading

Estimated review effort: 2/5 · risk: medium

📁 Walkthrough details

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js

  • Adds shared workspace selection and focus helpers for monitor isolation.
    • Filters dynamic, active, and fixed workspace lists by output.
    • Selects local active workspaces and calculates marker opacity.
    • Preserves uncreated fixed slots and beyond-range active workspaces.

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml

  • Applies monitor-aware workspace lists and active markers to the bar.
    • Uses local active workspace and focused-output state when isolation is enabled.
    • Dims active visuals on unfocused monitors, including Frame and Pacman styles.
    • Avoids tracking live workspace lists in non-isolated fixed mode.

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml

  • Filters the workspace popup by its screen when isolation is enabled.
    • Sorts displayed workspaces and derives navigation IDs from the same list.
    • Highlights the popup screen's active workspace.

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml

  • Adds the default-off isolateMonitors setting and persists it in QS Bar configuration.
    • Loads and writes qsbar.isolateMonitors.

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml

  • Passes the bar's screen name to the workspace widget.
    • Connects each widget instance to its output identity.

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml

  • Adds a per-monitor isolation toggle to Identity workspace settings.
    • Binds the on/off control to isolateMonitors.

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json

  • Exposes workspace mode, style, and isolation settings in the widget catalogue.
    • Enables catalogue-driven CLI control of isolateMonitors.

ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs

  • Adds tests for workspace selection, fixed slots, and monitor focus dimming.
    • Covers local active workspaces, data dependencies, and beyond-range slots.

ryoku/shell/ipc/bar_test.go

  • Tests enabling and disabling isolation through the bar IPC command.
    • Checks both values round-trip through the settings store.

ryoku/i18n/catalog/en.json

  • Adds English strings for the per-monitor setting and its explanation.
    • Registers the new Identity settings text.

CHANGELOG.md

  • Documents workspace isolation, focus dimming, and configuration entry points.
    • Adds an unreleased entry for issue 767.

Findings

Severity Category Fix effort File Issue
🟡 minor correctness medium ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml Possible Bug — With isolation enabled, fixed modes depend on every Wm.workspaces update, and workspaceList() returns a new array even when the displayed IDs have not changed. Focus or occupancy updates can therefore reset the repea…
🟢 trivial maintainability low ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml Maintainability — The new comment contains en-dash characters, which the repository's mechanical rules prohibit. Use ASCII punctuation instead.
🟢 trivial maintainability low ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml Maintainability — The new comment contains en-dash characters, which the repository's mechanical rules prohibit. Use ASCII punctuation instead.

⚡ Quick fixes by effort

Low effort (apply from the diff):

  • ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml:2345 — Maintainability — The new comment contains en-dash characters, which the repository's mechanical rules prohibit. Use ASCII punctuation instead.
  • ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml:307 — Maintainability — The new comment contains en-dash characters, which the repository's mechanical rules prohibit. Use ASCII punctuation instead.

Medium effort (a function-level change):

  • ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml:41 — Possible Bug — With isolation enabled, fixed modes depend on every Wm.workspaces update, and workspaceList() returns a new array even when the displayed IDs have not changed. Focus or occupancy updates can therefore reset the repea…
🤖 Prompt for AI agents (click, copy, paste into Claude/Codex)
Fix the following issues found by mika-agent on Ryoku-dev/ryoku#770 (branch feat/qsbar-workspace-isolate-monitors):

- ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml:41-45 — Possible Bug (minor severity, medium effort): With isolation enabled, fixed modes depend on every `Wm.workspaces` update, and `workspaceList()` returns a new array even when the displayed IDs have not changed. Focus or occupancy updates can therefore reset the repea…

For each: verify it is real in the current code, apply the smallest correct fix, and run the repo's tests. Skip anything already resolved and say why.
🛠 Deterministic checks run
  • golangci-lint: not installed (skip)

🚦 Merge recommendation: needs_review


🦊 Generated by Mika — the Ryoku org's self-hosted AI code reviewer. Machine findings: verify before merging.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +313 to +320
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.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Data Integrity · fix: medium effort

The new translation keys are not added to the source and English catalogs in this diff. The new settings card therefore lacks catalog entries for its title, label, and description, breaking translated-UI integrity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added missing English catalog entries for "Per-monitor" and "Each bar shows only the workspaces on its own screen." in ryoku/i18n/catalog/en.json.

if (!ws[i].special && ws[i].active && ws[i].output === wsWidget.screenName)
return ws[i]
}
return Wm.focusedWorkspace

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: low effort

If no active workspace is found for a screen, this falls back to the globally focused workspace, which can belong to another screen. In fixed mode an out-of-range global workspace is then appended to that screen's list and highlighted, breaking isolation; return no local active workspace instead.

Suggested change

Suggested change
return Wm.focusedWorkspace
return null

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Returned null instead of falling back to Wm.focusedWorkspace when isolateMonitors is active but no monitor-active workspace is found.

continue
res.push(vs[i])
}
res.sort(function(x, y) { return Number(x.name) - Number(y.name) })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: medium effort

The popup now sorts its visible workspaces even when isolation is disabled, whereas previously it displayed Wm.workspaces in provider order. If the provider returns a non-numeric order, opening the popup changes the displayed order despite isolation being off; preserve the original order in that mode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Only sort filteredWorkspaces when isolateMonitors is enabled, preserving original provider order when disabled.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +51 to +57
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)
if (!root.isolateMonitors) {
a.sort(function(x, y) { return Number(x) - Number(y) })
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: low effort

When isolation is off, the repeater preserves Wm.workspaces order but wsIds is sorted separately. If the compositor returns workspaces out of numeric order, keyboard navigation moves through them in a different order from the displayed rows. Use the same ordered model for both display and navigation.

…cePanel

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 Ryoku-dev#770.
@Satirito

Satirito commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in b753286.

The root cause: wsIds was sorted independently from filteredWorkspaces, so the two could diverge whenever the compositor returned workspaces out of numeric order and isolation was off — breaking keyboard navigation.

Fix applied:

  • filteredWorkspaces now always sorts numerically (guard if (root.isolateMonitors) removed).
  • wsIds is now a pure map over filteredWorkspaces, deriving names in the same order the repeater renders them.

This guarantees the two arrays are always aligned, so keyboard Up/Down always selects the visually highlighted row.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +451 to +453
// 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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Tests · fix: medium effort

The new test only checks persistence through the CLI, leaving the workspace filtering and per-monitor active marker untested. Add coverage for multiple outputs, including uncreated fixed-mode workspaces and focus changes.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +34 to +36
readonly property var workspaceList: WorkspaceIsolation.workspaceList(
Wm.workspaces, root.workspaceMode, wsWidget.dynamicModel, wsWidget.screenName,
root.isolateMonitors, root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: medium effort

In fixed workspace modes, this binding now depends directly on Wm.focusedWorkspace and returns a new array on every focus change, even when the displayed workspace IDs are unchanged. Replacing the Repeater model recreates its delegates, interrupting their focus animations; the previous extraWs property avoided model changes for in-range switches. Keep the model stable when its IDs have not changed.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +41 to +43
readonly property var workspaceList: WorkspaceIsolation.workspaceList(
Wm.workspaces, root.workspaceMode, wsWidget.dynamicModel, wsWidget.screenName,
root.isolateMonitors, wsWidget.listActiveWorkspace, wsWidget.extraWs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: medium effort

In fixed workspace modes, workspaceList now depends on Wm.workspaces even when isolation is disabled. A workspace update therefore produces a new array and resets the Repeater model, recreating delegates and interrupting their focus animations; the previous fixed model stayed stable for in-range focus changes. Avoid depending on the live workspace list in fixed mode when isolation is off.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.



// ── PREVIEW ──
// ── workspaces: ISOLATE ──

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 trivial · Maintainability · fix: low effort

The new comment contains en-dash characters, which the repository's mechanical rules prohibit. Use ASCII punctuation instead.

Suggested change

Suggested change
// ── workspaces: ISOLATE ──
// Workspaces: ISOLATE

Reply here to discuss, say it's fixed, or explain why it's intended — no mention needed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants