Skip to content

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

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

Satirito wants to merge 11 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 an optional, persisted per-monitor workspace isolation setting.
  • Filter workspace bars and popups by screen.
  • Highlight each screen's active workspace and dim unfocused indicators.
  • Cover workspace selection and CLI setting round-trips with tests.

Change diagram

flowchart LR
  Settings["Identity settings / CLI"] --> Theme["Theme configuration"]
  Theme --> Widget["Workspace widget"]
  Theme --> Panel["Workspace popup"]
  Screen["Bar screen"] --> Widget
  Wm["Workspace state"] --> Widget
  Wm --> Panel
  Widget --> Isolation["Workspace isolation logic"]
Loading

Estimated review effort: 2/5 · risk: medium

📁 Walkthrough details

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

  • Extracts screen-aware workspace selection and focus logic for the bar.
    • Filters dynamic, active, and fixed workspace lists by output.
    • Selects each monitor's active workspace and computes marker opacity.
    • Preserves uncreated fixed slots and supports active workspaces beyond the range.

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

  • Applies monitor-aware workspace lists, active markers, and focus dimming.
    • Uses the bar screen and isolation helpers to select workspace models.
    • Updates standard, rings, and Pacman visuals for per-monitor focus.

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

  • Filters the workspace popup by its screen when isolation is enabled.
    • Uses one sorted workspace list for delegates and keyboard navigation.
    • Highlights the popup screen's active workspace.

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

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

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

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

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

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

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

  • Passes the bar's screen name to WorkspaceWidget.
    • Connects workspace filtering to the bar's output.

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

  • Tests monitor-specific workspace selection and focus opacity.
    • Covers fixed slots, out-of-range active workspaces, and disabled isolation.

ryoku/shell/ipc/bar_test.go

  • Tests CLI toggling and settings-store round-trips for workspace isolation.
    • Adds the toggle to the test catalogue and checks on/off values.

ryoku/i18n/catalog/en.json

  • Adds English strings for the per-monitor settings control.
    • Registers the control label and explanatory text.

CHANGELOG.md

  • Documents workspace isolation, focus dimming, popup filtering, and configuration options.
    • Adds an unreleased entry for issue

Findings

Severity Category Fix effort File Issue
🟡 minor correctness medium ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml Possible Bug — 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 interrup…

⚡ Quick fixes by effort

Medium effort (a function-level change):

  • ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml:41 — Possible Bug — 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 interrup…
🤖 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-43 — Possible Bug (minor severity, 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 interrup…

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.

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