import QtQuick import Quickshell import Quickshell.Io import qs.Common import qs.Modules.Plugins import qs.Services import "./services" import "./overlay" PluginComponent { id: root pluginId: "gameBar" pluginService: PluginService readonly property bool overlayOpen: overlay.shouldBeVisible // Widget catalogue. `id` is the persistence key, the rest drives the top bar // toggles and the default placement (fractions of the screen size). readonly property var widgetDefs: [ { "id": "audio", "title": "Audio", "icon": "volume_up", "width": 430, "height": 470, "x": 0.03, "y": 0.14, "defaultVisible": true }, { "id": "apps", "title": "Apps", "icon": "tune", "width": 400, "height": 360, "x": 0.3, "y": 0.14, "defaultVisible": true }, { "id": "timer", "title": "Timer", "icon": "timer", "width": 380, "height": 320, "x": 0.57, "y": 0.58, "defaultVisible": false }, { "id": "capture", "title": "Capture", "icon": "videocam", "width": 380, "height": 230, "x": 0.03, "y": 0.62, "defaultVisible": false }, { "id": "media", "title": "Media", "icon": "music_note", "width": 430, "height": 300, "x": 0.3, "y": 0.52, "defaultVisible": true }, { "id": "home", "title": "Home", "icon": "home", "width": 420, "height": 420, "x": 0.57, "y": 0.14, "defaultVisible": false }, { "id": "visualizer", "title": "Visualiser", "icon": "graphic_eq", "width": 360, "height": 180, "x": 0.3, "y": 0.79, "defaultVisible": false }, { "id": "bluetooth", "title": "Bluetooth", "icon": "bluetooth", "width": 380, "height": 300, "x": 0.8, "y": 0.14, "defaultVisible": false }, { "id": "notepad", "title": "Notes", "icon": "edit_note", "width": 400, "height": 320, "x": 0.57, "y": 0.58, "defaultVisible": false }, { "id": "system", "title": "Performance", "icon": "speed", "width": 380, "height": 330, "x": 0.8, "y": 0.45, "defaultVisible": false } ] function widgetDef(widgetId) { return widgetDefs.find(def => def.id === widgetId) || null; } // ── Visibility ─────────────────────────────────────────────────────────── function savePluginValue(key, value) { if (pluginService) pluginService.savePluginData(pluginId, key, value); } function isWidgetVisible(widgetId) { const stored = pluginData.visibleWidgets; if (stored && stored[widgetId] !== undefined) return stored[widgetId] === true; const def = widgetDef(widgetId); return def ? def.defaultVisible === true : false; } function setWidgetVisible(widgetId, visible) { const stored = Object.assign({}, pluginData.visibleWidgets || {}); stored[widgetId] = visible === true; root.savePluginValue("visibleWidgets", stored); } function toggleWidget(widgetId) { root.setWidgetVisible(widgetId, !root.isWidgetVisible(widgetId)); } // ── Positions ──────────────────────────────────────────────────────────── // Stored per screen as fractions of the screen size, so the same layout // survives monitors of different resolutions (and is re-clamped on load). // ── Brightness device mapping ──────────────────────────────────────────── // DisplayService only knows DDC devices as i2c bus ids, and the shell maps // them to screens through manual pins. "ddcutil detect" reports the DRM // connector for each bus, which is the Wayland output name, so the mapping // can be worked out once and cached. property var brightnessMap: ({}) property bool brightnessMapScanning: false // Picks the cached map up as soon as plugin data arrives; surfaces are // created before the daemon registers, so they cannot drive this. readonly property var storedBrightnessMap: (pluginData && pluginData.brightnessMap && typeof pluginData.brightnessMap === "object") ? pluginData.brightnessMap : ({}) onStoredBrightnessMapChanged: { if (Object.keys(root.storedBrightnessMap).length > 0) root.brightnessMap = root.storedBrightnessMap; root.ensureBrightnessMap(); } function brightnessDeviceForScreen(screenName) { if (!screenName) return ""; // A pin set in the shell's own UI always wins. const pins = (typeof CacheData !== "undefined" && CacheData.brightnessDevicePins) ? CacheData.brightnessDevicePins : {}; const screen = Quickshell.screens.find(s => s.name === screenName); const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName; if (pins[pinKey] && DisplayService.devices.some(d => d.id === pins[pinKey])) return pins[pinKey]; const mapped = root.brightnessMap[screenName]; if (mapped && DisplayService.devices.some(d => d.id === mapped)) return mapped; const backlight = DisplayService.devices.find(d => d.class === "backlight"); if (backlight) return backlight.id; const ddc = DisplayService.devices.find(d => d.class === "ddc"); return ddc ? ddc.id : ""; } // The shell's own Control Center brightness slider resolves its device from // per-screen pins, so publishing the detected map there makes the built-in // slider (and its icon menu) follow whichever monitor the panel is on. function syncBrightnessPins() { if (pluginData.autoPinBrightness === false) return; if (typeof CacheData === "undefined" || !CacheData) return; const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {})); let changed = false; for (const screen of Quickshell.screens) { const device = root.brightnessMap[screen.name]; if (!device) continue; const key = SettingsData.getScreenDisplayName(screen) || screen.name; // Never overwrite a pin the user set by hand. if (pins[key]) continue; pins[key] = device; changed = true; } if (changed) CacheData.set("brightnessDevicePins", pins); } function ensureBrightnessMap() { if (root.brightnessMapScanning) return; if (Object.keys(root.brightnessMap).length === 0 && Object.keys(root.storedBrightnessMap).length > 0) root.brightnessMap = root.storedBrightnessMap; const screens = Quickshell.screens.map(s => s.name); const missing = screens.filter(name => !root.brightnessMap[name]); if (missing.length === 0) { root.syncBrightnessPins(); return; } root.brightnessMapScanning = true; Proc.runCommand("gameBar.ddcDetect", ["ddcutil", "detect", "--brief"], (stdout, exitCode) => { root.brightnessMapScanning = false; if (exitCode !== 0) return; const map = {}; let bus = ""; for (const line of String(stdout || "").split("\n")) { const busMatch = line.match(/I2C bus:\s*\/dev\/i2c-([0-9]+)/); if (busMatch) { bus = "ddc:i2c-" + busMatch[1]; continue; } const connectorMatch = line.match(/DRM connector:\s*card[0-9]+-(.+)$/); if (connectorMatch && bus !== "") { map[connectorMatch[1].trim()] = bus; bus = ""; } } if (Object.keys(map).length === 0) return; root.brightnessMap = map; root.savePluginValue("brightnessMap", map); root.syncBrightnessPins(); }, 0, 20000); } function focusedMonitorName() { const screen = CompositorService.getFocusedScreen(); return (screen && screen.name) ? screen.name : "focused"; } // Positions are stored per monitor *and* per resolution: a layout arranged at // 4K means nothing at 1440p, and switching back must restore the original // arrangement rather than a rescaled approximation of the other one. function screenKey(screen) { if (!screen) return "default"; const base = SettingsData.getScreenDisplayName(screen) || screen.name || "default"; const w = Math.round(screen.width); const h = Math.round(screen.height); return (w > 0 && h > 0) ? base + "@" + w + "x" + h : base; } function legacyScreenKey(screen) { if (!screen) return ""; return SettingsData.getScreenDisplayName(screen) || screen.name || ""; } // Returns absolute pixel coordinates for the given panel size. function widgetPosition(screen, widgetId, panelWidth, panelHeight) { const layout = pluginData.layout || {}; const perScreen = layout[root.screenKey(screen)]; const saved = perScreen ? perScreen[widgetId] : null; if (saved && saved.x !== undefined && saved.y !== undefined) return { "x": saved.x, "y": saved.y }; // Entries written before 2.5.0 were fractions keyed by screen name only. const legacyPerScreen = layout[root.legacyScreenKey(screen)]; const legacy = legacyPerScreen ? legacyPerScreen[widgetId] : null; if (legacy && legacy.x !== undefined && legacy.y !== undefined && legacy.x <= 1 && legacy.y <= 1) return { "x": legacy.x * panelWidth, "y": legacy.y * panelHeight }; const def = widgetDef(widgetId); return { "x": (def ? def.x : 0.1) * panelWidth, "y": (def ? def.y : 0.15) * panelHeight }; } function saveWidgetPosition(screen, widgetId, x, y) { const layout = JSON.parse(JSON.stringify(pluginData.layout || {})); const key = root.screenKey(screen); if (!layout[key]) layout[key] = {}; layout[key][widgetId] = { "x": Math.round(x), "y": Math.round(y) }; root.savePluginValue("layout", layout); } function resetLayout() { root.savePluginValue("layout", {}); } // ── Overlay ────────────────────────────────────────────────────────────── function openOverlay() { // No early return on shouldBeVisible: the flag can survive a shell // restart with no window mapped, which used to make open() a no-op. if (typeof PopoutService !== "undefined" && PopoutService) PopoutService.closeControlCenter(); overlay.shouldBeVisible = true; overlay.open(); } function closeOverlay() { if (!overlay.shouldBeVisible) return; overlay.shouldBeVisible = false; overlay.close(); } function toggleOverlay() { if (overlay.shouldBeVisible) root.closeOverlay(); else root.openOverlay(); } IpcHandler { target: "gameBar" enabled: true function toggle() : string { root.toggleOverlay(); return "SUCCESS"; } function open() : string { root.openOverlay(); return "SUCCESS"; } function close() : string { root.closeOverlay(); return "SUCCESS"; } function widget(name: string) : string { if (!root.widgetDef(name)) return "UNKNOWN_WIDGET: " + name; root.toggleWidget(name); return "SUCCESS"; } function homeToggle(entityId: string) : string { if (!entityId) return "USAGE: homeToggle "; const domain = haService.domainOf(entityId); if (["scene", "script", "button", "input_button"].indexOf(domain) !== -1) haService.press(entityId); else haService.toggle(entityId); return "SUCCESS"; } function record() : string { // The recorder stops asynchronously, so report the action taken. const wasRecording = capture.recording; capture.toggleRecording(root.focusedMonitorName()); return wasRecording ? "STOPPED" : "RECORDING"; } function replay() : string { const wasArmed = capture.replayArmed; capture.toggleReplay(root.focusedMonitorName()); return wasArmed ? "STOPPED" : "ARMED"; } function saveReplay() : string { if (!capture.replayArmed) return "REPLAY_NOT_ARMED"; capture.saveReplay(); return "SUCCESS"; } function timer(seconds: string, label: string) : string { const value = parseInt(seconds, 10); if (isNaN(value) || value <= 0) return "USAGE: timer [label]"; timers.resetTimer(); timers.startTimer(value * 1000, label || ""); return "STARTED " + timers.formatDuration(value * 1000); } function timerStop() : string { timers.resetTimer(); return "SUCCESS"; } function timerStatus() : string { return "timer=" + (timers.timerRunning ? "running" : (timers.timerFinished ? "finished" : "idle")) + " remaining=" + timers.formatDuration(timers.remainingMs) + " label=" + (timers.timerLabel || "(none)") + " stopwatch=" + (timers.stopwatchRunning ? "running" : "idle") + " elapsed=" + timers.formatStopwatch(timers.stopwatchElapsedMs); } function layoutStatus() : string { const lines = Quickshell.screens.map(screen => { const key = root.screenKey(screen); const parts = root.widgetDefs.map(def => { const pos = root.widgetPosition(screen, def.id, screen.width, screen.height); return def.id + "(" + Math.round(pos.x) + "," + Math.round(pos.y) + ")"; }); return key + ": " + parts.join(" "); }); return lines.join(" | "); } function brightnessStatus() : string { const pins = (typeof CacheData !== "undefined" && CacheData.brightnessDevicePins) ? CacheData.brightnessDevicePins : {}; const perScreen = Quickshell.screens.map(screen => { const key = SettingsData.getScreenDisplayName(screen) || screen.name; return screen.name + "[key=" + key + " device=" + root.brightnessDeviceForScreen(screen.name) + " value=" + DisplayService.getDeviceBrightness(root.brightnessDeviceForScreen(screen.name)) + "]"; }); return "map=" + JSON.stringify(root.brightnessMap) + " pins=" + JSON.stringify(pins) + " screens=" + perScreen.join(" ") + " currentDevice=" + DisplayService.currentDevice; } function captureStatus() : string { return "mode=" + (capture.mode || "idle") + " monitor=" + (capture.targetMonitor || "-") + " elapsed=" + capture.elapsedSeconds + "s dir=" + capture.saveDirectory + " lastFile=" + (capture.lastFile || "(none)") + " error=" + (capture.lastError || "(none)"); } function homeStatus() : string { const picked = Array.isArray(root.pluginData.haEntities) ? root.pluginData.haEntities.length : 0; return "configured=" + haService.configured + " url=" + (haService.baseUrl || "(none)") + " tokenChars=" + haService.token.length + " http=" + haService.lastStatus + " entities=" + haService.entityList.length + " picked=" + picked + " error=" + (haService.lastError || "(none)"); } function homeRefresh() : string { haService.refresh(); return "SUCCESS"; } function resetLayout() : string { root.resetLayout(); return "SUCCESS"; } } readonly property alias homeService: haService readonly property alias captureService: capture readonly property alias timerService: timers TimerService { id: timers daemon: root } CaptureService { id: capture daemon: root onNotice: message => { if (typeof ToastService !== "undefined" && ToastService) ToastService.showInfo(message); } } HomeService { id: haService daemon: root } Connections { target: Quickshell function onScreensChanged() { root.ensureBrightnessMap(); } } OverlayModal { id: overlay daemon: root } Component.onCompleted: { root.ensureBrightnessMap(); if (pluginService && pluginId) { const instances = Object.assign({}, pluginService.pluginInstances); instances[pluginId] = root; pluginService.pluginInstances = instances; } } Component.onDestruction: { if (pluginService && pluginService.pluginInstances[pluginId] === root) { const instances = Object.assign({}, pluginService.pluginInstances); delete instances[pluginId]; pluginService.pluginInstances = instances; } } }