Repository navigation
Conversation
A passive overlay per output, raised with SUPER + SHIFT + G over a fullscreen game: a strip of widget buttons, draggable and resizable cards, edge and grid snapping with guides, and a settle curve. The cards are capture, performance, resources, tasks, media, voice, audio and gallery, and each reads only while it is on screen. Discord Voice reads the official client over native RPC, or Vesktop through a Vencord companion that publishes the call on a local socket. A pinned voice card shrinks to a content-sized overlay in the corner the card was placed, stays passive when the bar is closed, and stops the bridge when Voice is not shown. The Hub gains a Game Bar page for the master switch, placement, motion and the voice overlay, every key stored under gamebar in shell.json. The keybind is registered in every window manager. Note: New: the Game Bar overlay (SUPER + SHIFT + G) with widgets and a pinned Discord Voice overlay Note: Fixed: Hub step rows now keep to the schema range
🦊 mika's reviewIncremental review: new commits since the last checkpoint. Summary
Change diagramflowchart LR
Bind["Compositor shortcut / shell IPC"] --> Overlay["Game Bar overlay"]
Hub["Hub settings"] --> Config["Settings daemon"]
Config --> Overlay
Overlay --> Store["Persisted layout"]
Overlay --> Cards["Capture, system, media and audio cards"]
Overlay --> Voice["Discord voice service"]
Voice --> Bridge["Python bridge"]
Bridge --> RPC["Discord RPC"]
Bridge --> Companion["Vencord companion socket"]
Estimated review effort: 3/5 · risk: medium 📁 Walkthrough details
|
| Severity | Category | Fix effort | File | Issue |
|---|---|---|---|---|
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/GameBar.qml |
Possible Bug — exitTimer is not stopped when the surface becomes active again. If the bar is reopened before its closing fade finishes, the timer fires while wanted is true and leaves stripUp unchanged; more importantly, a pendin… |
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/GameBarTaskRow.qml |
Bug — Limit the task name to the space before the CPU column. Currently, long names overlap both the CPU and memory values. |
| 🟡 minor | correctness | medium | ryoku/shell/quickshell/shell/modules/gamebar/cards/AudioCard.qml |
Possible Bug — The app list is limited to the first three streams with no scrolling or selection. When more than three applications are playing audio, users cannot adjust the remaining application volumes from the Game Bar. Make the li… |
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/cards/GalleryCard.qml |
Bug — model.get() does not establish a QML binding dependency on model changes, so the gallery can remain empty after the folder loads or stale after captures are added or removed. Set the model's sort order to newest first… |
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/cards/PerformanceCard.qml |
Bug — The state read runs only when the card is created; if monitorName is assigned or changes afterward, the refresh rate and resolution can remain associated with the wrong output. Restart the read when monitorName chang… |
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py |
Bug — Escape non-ASCII characters when emitting JSON. The bridge runs with a pipe on stdout, whose encoding can be ASCII under some locales; a Discord display name containing non-ASCII characters would otherwise terminate the… |
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py |
Bug — Bound the handshake read so an RPC socket that accepts connections but never sends READY cannot block the input loop indefinitely. Catch the timeout alongside the existing connection failures so the next candidate is t… |
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/native.ts |
Bug — An old socket can close after a replacement starts connecting; its handler then clears the replacement's connecting flag and input buffer. Reset connection state only when the closing socket is still current. |
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/services/DiscordVoice.qml |
Bug — Discord CDN avatar sizes must be powers of two; the default 96 produces an invalid image URL. Use 128 as the default so participant avatars load. |
⚡ Quick fixes by effort
Low effort (apply from the diff):
ryoku/shell/quickshell/shell/modules/gamebar/cards/GalleryCard.qml— Refresh gallery items when folders changeryoku/shell/quickshell/shell/modules/gamebar/cards/PerformanceCard.qml— Refresh display mode when output changesryoku/shell/quickshell/shell/modules/gamebar/GameBarTaskRow.qml— Prevent task names overlapping metricsryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py— Escape Unicode in bridge outputryoku/shell/quickshell/shell/services/DiscordVoice.qml— Use a valid Discord avatar sizeryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py— Bound the RPC handshake waitryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/native.ts— Preserve replacement socket stateryoku/shell/quickshell/shell/modules/gamebar/cards/GalleryCard.qml:44 — Bug —model.get()does not establish a QML binding dependency on model changes, so the gallery can remain empty after the folder loads or stale after captures are added or removed. Set the model's sort order to newest first…ryoku/shell/quickshell/shell/modules/gamebar/cards/PerformanceCard.qml:32 — Bug — The state read runs only when the card is created; ifmonitorNameis assigned or changes afterward, the refresh rate and resolution can remain associated with the wrong output. Restart the read whenmonitorNamechang…ryoku/shell/quickshell/shell/modules/gamebar/GameBarTaskRow.qml:29 — Bug — Limit the task name to the space before the CPU column. Currently, long names overlap both the CPU and memory values.ryoku/shell/quickshell/shell/modules/gamebar/GameBar.qml:384 — Possible Bug —exitTimeris not stopped when the surface becomes active again. If the bar is reopened before its closing fade finishes, the timer fires whilewantedis true and leavesstripUpunchanged; more importantly, a pendin…ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py:34 — Bug — Escape non-ASCII characters when emitting JSON. The bridge runs with a pipe on stdout, whose encoding can be ASCII under some locales; a Discord display name containing non-ASCII characters would otherwise terminate the…ryoku/shell/quickshell/shell/services/DiscordVoice.qml:86 — Bug — Discord CDN avatar sizes must be powers of two; the default96produces an invalid image URL. Use128as the default so participant avatars load.ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py:125 — Bug — Bound the handshake read so an RPC socket that accepts connections but never sendsREADYcannot block the input loop indefinitely. Catch the timeout alongside the existing connection failures so the next candidate is t…ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/native.ts:67 — Bug — An old socket can close after a replacement starts connecting; its handler then clears the replacement'sconnectingflag and input buffer. Reset connection state only when the closing socket is still current.
Medium effort (a function-level change):
ryoku/shell/quickshell/shell/modules/gamebar/cards/AudioCard.qml:70 — Possible Bug — The app list is limited to the first three streams with no scrolling or selection. When more than three applications are playing audio, users cannot adjust the remaining application volumes from the Game Bar. Make the li…
🤖 Prompt for AI agents (click, copy, paste into Claude/Codex)
Fix the following issues found by mika-agent on Ryoku-dev/ryoku#771 (branch ryo-gamebar):
- ryoku/shell/quickshell/shell/modules/gamebar/cards/GalleryCard.qml:44-46 — Bug (minor severity, low effort): `model.get()` does not establish a QML binding dependency on model changes, so the gallery can remain empty after the folder loads or stale after captures are added or removed. Set the model's sort order to newest first…
suggested fix:
readonly property var shots: {
shotFiles.count;
return card.newest(shotFiles, 6, "shot");
}
readonly property var clips: {
clipFiles.count;
return card.newest(clipFiles, 6, "clip");
}
readonly property var items: card.mode === "clips" ? card.clips : card.shots
- ryoku/shell/quickshell/shell/modules/gamebar/cards/PerformanceCard.qml:32-36 — Bug (minor severity, low effort): The state read runs only when the card is created; if `monitorName` is assigned or changes afterward, the refresh rate and resolution can remain associated with the wrong output. Restart the read when `monitorName` chang…
suggested fix:
Component.onCompleted: {
Sysinfo.setActive(card, true);
StatsFeed.setActive(card, true);
if (!modeRead.running)
modeRead.running = true;
}
onMonitorNameChanged: {
card.displayMode = "";
if (modeRead.running)
modeRead.running = false;
modeRead.running = true;
}
- ryoku/shell/quickshell/shell/modules/gamebar/GameBarTaskRow.qml:29-38 — Bug (minor severity, low effort): Limit the task name to the space before the CPU column. Currently, long names overlap both the CPU and memory values.
suggested fix:
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Math.max(0, cpu.x - 10 * root.s)
text: root.task ? root.task.name : ""
color: GameBarLook.fg
font.family: GameBarLook.face
font.pixelSize: GameBarLook.small * root.s
elide: Text.ElideRight
}
- ryoku/shell/quickshell/shell/modules/gamebar/GameBar.qml:384-390 — Possible Bug (minor severity, low effort): `exitTimer` is not stopped when the surface becomes active again. If the bar is reopened before its closing fade finishes, the timer fires while `wanted` is true and leaves `stripUp` unchanged; more importantly, a pendin…
- ryoku/shell/quickshell/shell/modules/gamebar/cards/AudioCard.qml:70-71 — Possible Bug (minor severity, medium effort): The app list is limited to the first three streams with no scrolling or selection. When more than three applications are playing audio, users cannot adjust the remaining application volumes from the Game Bar. Make the li…
- ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py:34-35 — Bug (minor severity, low effort): Escape non-ASCII characters when emitting JSON. The bridge runs with a pipe on stdout, whose encoding can be ASCII under some locales; a Discord display name containing non-ASCII characters would otherwise terminate the…
suggested fix:
def emit(kind: str, **fields: Any) -> None:
print(json.dumps({"type": kind, **fields}, separators=(",", ":"), ensure_ascii=True), flush=True)
- ryoku/shell/quickshell/shell/services/DiscordVoice.qml:86-86 — Bug (minor severity, low effort): Discord CDN avatar sizes must be powers of two; the default `96` produces an invalid image URL. Use `128` as the default so participant avatars load.
suggested fix:
- ".png?size=" + (size || 128);
- ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py:125-128 — Bug (minor severity, low effort): Bound the handshake read so an RPC socket that accepts connections but never sends `READY` cannot block the input loop indefinitely. Catch the timeout alongside the existing connection failures so the next candidate is t…
suggested fix:
reader, writer = await asyncio.open_unix_connection(path)
self.reader, self.writer, self.rpc_path = reader, writer, path
await self.send_frame({"v": 1, "client_id": APP_ID}, HANDSHAKE)
try:
opcode, ready = await asyncio.wait_for(self.receive(reader), AUTHORIZE_TIMEOUT)
except asyncio.TimeoutError as error:
raise ConnectionError("the RPC handshake timed out") from error
- ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/native.ts:67-71 — Bug (minor severity, low effort): An old socket can close after a replacement starts connecting; its handler then clears the replacement's `connecting` flag and input buffer. Reset connection state only when the closing socket is still current.
suggested fix:
candidate.on("close", () => {
if (socket === candidate) {
reset();
socket = undefined;
}
});
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)
- shellcheck: not installed (skip)
🚦 Merge recommendation: needs_review
🦊 Generated by Mika — the Ryoku org's self-hosted AI code reviewer. Machine findings: verify before merging.
Only the active output fills its cards, card loaders wait for the surface to be live, the exit timer follows the fade, and capture suppression follows the selection overlay instead of polling. Gallery gains a screenshots/recordings switch with play tiles and file names. Voice delegates bind the participant explicitly. The bridge ignores non-object companion messages, the companion clears queued lines on disconnect, the installer cannot swallow a failed build, and the tasks card reads cputimes as seconds. Note: Fixed: the Game Bar hides for captures, lists recordings, shows Voice participants and reports real CPU use
SelectedChannelStore may return no channel, and the companion build accepts the undefined case at runtime. Say so in the signature so tsc --noEmit passes.
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| case "audio": return audioComp; | ||
| case "gallery": return galleryComp; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| // The bar must not be in its own capture: unmap the surface, run the | ||
| // existing shot path, and restore after the selection overlay finishes. | ||
| function shoot(mode) { |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
A pending restoreDelay from a previous capture can fire during a new selection and remap the overlay into the screenshot. Stop it before starting the new capture.
When a screenshot is cancelled before Capture.selecting ever becomes nonempty, no restore timer is scheduled and suppressed remains true. Add a fallback that restores the overlay if selection never starts.
Suggested change
| case "audio": return audioComp; | |
| case "gallery": return galleryComp; | |
| } | |
| return null; | |
| } | |
| // The bar must not be in its own capture: unmap the surface, run the | |
| // existing shot path, and restore after the selection overlay finishes. | |
| function shoot(mode) { | |
| function shoot(mode) { | |
| if (Capture.selecting !== "") | |
| return; | |
| restoreDelay.stop(); | |
| win.sawSelecting = false; | |
| win.suppressed = true; | |
| Capture.shoot(mode); | |
| if (mode === "all" || Capture.selecting === "") | |
| win.restoreAfter(Capture.delay * 1000 + 800); | |
| } |
| function startRecording() { | ||
| win.suppressed = true; | ||
| Recorder.startAfter(Recorder.recordArgs(), 1); | ||
| win.restoreAfter(1900); |
There was a problem hiding this comment.
🟠 major · Possible Bug · fix: medium effort
Recording restores the overlay after 1.9 seconds even while recording continues. A screen or monitor recording therefore includes the Game Bar whenever it remains open or pinned; keep it suppressed until recording stops.
| implicitHeight: 32 * root.s | ||
| implicitWidth: 276 * root.s | ||
|
|
||
| onTaskChanged: confirming = false |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
Each process refresh replaces the row's task, resetting confirming every two seconds. A user who takes longer than the refresh interval to confirm an End action loses the confirmation; retain it while the PID remains the same.
| Text { | ||
| anchors.left: parent.left | ||
| anchors.verticalCenter: parent.verticalCenter | ||
| width: Math.max(0, actions.x - 12 * root.s) | ||
| text: root.task ? root.task.name : "" |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
The process name extends into the CPU and memory columns because its width is based on actions.x. Limit it to the start of the CPU column so long names cannot obscure the metrics.
Suggested change
| Text { | |
| anchors.left: parent.left | |
| anchors.verticalCenter: parent.verticalCenter | |
| width: Math.max(0, actions.x - 12 * root.s) | |
| text: root.task ? root.task.name : "" | |
| Text { | |
| anchors.left: parent.left | |
| anchors.verticalCenter: parent.verticalCenter | |
| width: Math.max(0, cpu.x - 12 * root.s) | |
| text: root.task ? root.task.name : "" |
| readonly property var shots: card.newest(shotFiles, 6, "shot") | ||
| readonly property var clips: card.newest(clipFiles, 6, "clip") | ||
| readonly property var items: card.mode === "clips" ? card.clips : card.shots |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
newest() reads model entries through get(), which does not establish a binding dependency on model changes. Depend on each model's count so the gallery refreshes when captures are added or removed.
Suggested change
| readonly property var shots: card.newest(shotFiles, 6, "shot") | |
| readonly property var clips: card.newest(clipFiles, 6, "clip") | |
| readonly property var items: card.mode === "clips" ? card.clips : card.shots | |
| readonly property var shots: { | |
| shotFiles.count; | |
| return card.newest(shotFiles, 6, "shot"); | |
| } | |
| readonly property var clips: { | |
| clipFiles.count; | |
| return card.newest(clipFiles, 6, "clip"); | |
| } | |
| readonly property var items: card.mode === "clips" ? card.clips : card.shots |
| protectedName: Tasks.isProtected(modelData.name) | ||
| onEndRequested: pid => { | ||
| killer.command = ["kill", "-TERM", String(pid)]; | ||
| killer.running = true; | ||
| settle.restart(); |
There was a problem hiding this comment.
🟡 minor · Possible bug · fix: low effort
ps lists processes owned by other users, and kill inherits the shell's privileges. Prevent ending another user's processes by limiting the displayed process list to the current user.
confidence: 60/100
Suggested change
| protectedName: Tasks.isProtected(modelData.name) | |
| onEndRequested: pid => { | |
| killer.command = ["kill", "-TERM", String(pid)]; | |
| killer.running = true; | |
| settle.restart(); | |
| onEndRequested: pid => { | |
| killer.command = ["kill", "-TERM", String(pid)]; | |
| killer.running = true; | |
| settle.restart(); | |
| } |
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| onActiveChanged: { | ||
| if (win.active) { | ||
| win.live = true; | ||
| win.shownCards = win.cards; | ||
| } else { | ||
| exitTimer.restart(); | ||
| } |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: low effort
exitTimer is not stopped when the surface becomes active again. If the bar is reopened before its closing fade finishes, the timer fires while wanted is true and leaves stripUp unchanged; more importantly, a pending timer from a monitor change can clear shownCards on the old surface while state is changing. Cancel the timer on reactivation and test rapid toggles and monitor changes.
confidence: 65/100
| Repeater { | ||
| model: card.streams.slice(0, 3) |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
The app list is limited to the first three streams with no scrolling or selection. When more than three applications are playing audio, users cannot adjust the remaining application volumes from the Game Bar. Make the list scrollable or otherwise expose all streams.
| Component.onCompleted: { | ||
| Sysinfo.setActive(card, true); | ||
| StatsFeed.setActive(card, true); | ||
| modeRead.running = true; | ||
| } |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
The state read runs only when the card is created; if monitorName is assigned or changes afterward, the refresh rate and resolution can remain associated with the wrong output. Restart the read when monitorName changes, clearing the previous mode first.
Suggested change
| Component.onCompleted: { | |
| Sysinfo.setActive(card, true); | |
| StatsFeed.setActive(card, true); | |
| modeRead.running = true; | |
| } | |
| Component.onCompleted: { | |
| Sysinfo.setActive(card, true); | |
| StatsFeed.setActive(card, true); | |
| if (!modeRead.running) | |
| modeRead.running = true; | |
| } | |
| onMonitorNameChanged: { | |
| card.displayMode = ""; | |
| if (modeRead.running) | |
| modeRead.running = false; | |
| modeRead.running = true; | |
| } |
| def emit(kind: str, **fields: Any) -> None: | ||
| print(json.dumps({"type": kind, **fields}, separators=(",", ":")), flush=True) |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Escape non-ASCII characters when emitting JSON. The bridge runs with a pipe on stdout, whose encoding can be ASCII under some locales; a Discord display name containing non-ASCII characters would otherwise terminate the process.
Suggested change
| def emit(kind: str, **fields: Any) -> None: | |
| print(json.dumps({"type": kind, **fields}, separators=(",", ":")), flush=True) | |
| def emit(kind: str, **fields: Any) -> None: | |
| print(json.dumps({"type": kind, **fields}, separators=(",", ":"), ensure_ascii=True), flush=True) |
| reader, writer = await asyncio.open_unix_connection(path) | ||
| self.reader, self.writer, self.rpc_path = reader, writer, path | ||
| await self.send_frame({"v": 1, "client_id": APP_ID}, HANDSHAKE) | ||
| opcode, ready = await self.receive(reader) |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Bound the handshake read so an RPC socket that accepts connections but never sends READY cannot block the input loop indefinitely. Catch the timeout alongside the existing connection failures so the next candidate is tried.
Suggested change
| reader, writer = await asyncio.open_unix_connection(path) | |
| self.reader, self.writer, self.rpc_path = reader, writer, path | |
| await self.send_frame({"v": 1, "client_id": APP_ID}, HANDSHAKE) | |
| opcode, ready = await self.receive(reader) | |
| reader, writer = await asyncio.open_unix_connection(path) | |
| self.reader, self.writer, self.rpc_path = reader, writer, path | |
| await self.send_frame({"v": 1, "client_id": APP_ID}, HANDSHAKE) | |
| try: | |
| opcode, ready = await asyncio.wait_for(self.receive(reader), AUTHORIZE_TIMEOUT) | |
| except asyncio.TimeoutError as error: | |
| raise ConnectionError("the RPC handshake timed out") from error |

What changed
Adds the Game Bar, like Windows, a passive overlay per output raised with
Super+Shift+Gover the focused monitor. The strip carries eight widget buttons plus a
snapping toggle, clock and battery. The cards are capture, performance,
resources, tasks, media, voice, audio and gallery. Cards drag from their frame,
snap to screen edges, centre lines and each other with a guide grid, and keep
their position and size across restarts. A pinned card stays after the bar
closes and never takes pointer or keyboard focus; a pinned Voice card shrinks
to a content-sized panel in the corner its full card occupied.
Discord Voice reads the official client over native RPC, or Vesktop and Equibop
through a bundled Vencord companion that publishes the call over a local Unix
socket. The card offers to build and install that companion, points the
client's
state.jsonat the builtdist, and keeps a backup. The bridge runsonly while the widget is on screen.
Ryoku Hub gains a Game Bar page under Desktop for the master switch, dim, scale,
radius, fade, snapping and guides, settle spring, and the voice overlay. Every
key lives under
gamebarinshell.json.Docs: new
docs/gamebar.md, linked fromdocs/structure.md.CHANGELOG.mdcarries the New and Fixed entries.
Area
ryoku
How it was tested
Super+Shift+Gover a real fullscreen game on a1920x1080 output. All eight cards rendered and were working as they were expected; the bar closed cleanly.
Official Discord over native RPC: authorization, channel and participant
list, speaking state, mute and deafen.
Vesktop companion:
install_companion.sh --client vesktopcloned Vencord,installed with
pnpm install --frozen-lockfileand built withpnpm build.The plugin is present in
dist/renderer.jsanddist/vencordDesktopMain.js,state.jsonpointsvencordDiratdist, and a backupstate.json.pre-ryoku-voiceexists. After restarting Vesktop, the Voice cardshowed the channel
Genel,1 person in voice, backendVesktopandparticipant
dodo985(me);ss -xshowed an established connection to/run/user/1000/ryoku-discord-voice.sock.luac -pon the Hyprland bind module,bash -non the installer,python3 -m py_compileon the bridge, andbin/ryoku-dev-lint-qmlon theshell and Hub roots. The layout, tasks and units JS tests pass. Commit hooks
pass without
--no-verify.Checklist
[area] scope: summarycommit subject.CHANGELOG.mdupdated in the area I touched.--no-verify.luac -p), shell scripts passbash -n, and QML passesqmllintwhere applicable.