diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Services/CompositorService.qml')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/dms/Services/CompositorService.qml | 677 |
1 files changed, 677 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Services/CompositorService.qml b/raveos-hyprland-theme/theme-data/dms/Services/CompositorService.qml new file mode 100644 index 0000000..d327ea2 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Services/CompositorService.qml @@ -0,0 +1,677 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.I3 +import Quickshell.Wayland +import Quickshell.Hyprland +import qs.Common + +Singleton { + id: root + + property bool isHyprland: false + property bool isNiri: false + property bool isDwl: false + property bool isSway: false + property bool isScroll: false + property bool isMiracle: false + property bool isLabwc: false + property string compositor: "unknown" + readonly property bool useHyprlandFocusGrab: isHyprland && Quickshell.env("DMS_HYPRLAND_EXCLUSIVE_FOCUS") !== "1" + + readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") + readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") + readonly property string swaySocket: Quickshell.env("SWAYSOCK") + readonly property string scrollSocket: Quickshell.env("SWAYSOCK") + readonly property string miracleSocket: Quickshell.env("MIRACLESOCK") + readonly property string labwcPid: Quickshell.env("LABWC_PID") + property bool useNiriSorting: isNiri && NiriService + + property var randrScales: ({}) + property bool randrReady: false + signal randrDataReady + + property var sortedToplevels: [] + property bool _sortScheduled: false + + signal toplevelsChanged + + function fetchRandrData() { + Proc.runCommand("randr", ["dms", "randr", "--json"], (output, exitCode) => { + if (exitCode === 0 && output) { + try { + const data = JSON.parse(output.trim()); + if (data.outputs && Array.isArray(data.outputs)) { + const scales = {}; + for (const out of data.outputs) { + if (out.name && out.scale > 0) + scales[out.name] = out.scale; + } + randrScales = scales; + } + } catch (e) { + console.warn("CompositorService: failed to parse randr data:", e); + } + } + randrReady = true; + randrDataReady(); + }, 0, 3000); + } + + function getScreenScale(screen) { + if (!screen) + return 1; + + if (Quickshell.env("QT_WAYLAND_FORCE_DPI") || Quickshell.env("QT_SCALE_FACTOR")) { + return screen.devicePixelRatio || 1; + } + + const randrScale = randrScales[screen.name]; + if (randrScale !== undefined && randrScale > 0) + return Math.round(randrScale * 20) / 20; + + if (WlrOutputService.wlrOutputAvailable && screen) { + const wlrOutput = WlrOutputService.getOutput(screen.name); + if (wlrOutput?.enabled && wlrOutput.scale !== undefined && wlrOutput.scale > 0) { + return Math.round(wlrOutput.scale * 20) / 20; + } + } + + if (isNiri && screen) { + const niriScale = NiriService.displayScales[screen.name]; + if (niriScale !== undefined) + return niriScale; + } + + if (isHyprland && screen) { + const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name); + if (hyprlandMonitor?.scale !== undefined) + return hyprlandMonitor.scale; + } + + if (isDwl && screen) { + const dwlScale = DwlService.getOutputScale(screen.name); + if (dwlScale !== undefined && dwlScale > 0) + return dwlScale; + } + + return screen?.devicePixelRatio || 1; + } + + function getFocusedScreen() { + let screenName = ""; + if (isHyprland && Hyprland.focusedWorkspace?.monitor) + screenName = Hyprland.focusedWorkspace.monitor.name; + else if (isNiri && NiriService.currentOutput) + screenName = NiriService.currentOutput; + else if (isSway || isScroll || isMiracle) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + screenName = focusedWs?.monitor?.name || ""; + } else if (isDwl && DwlService.activeOutput) + screenName = DwlService.activeOutput; + + if (!screenName) + return Quickshell.screens.length > 0 ? Quickshell.screens[0] : null; + + for (let i = 0; i < Quickshell.screens.length; i++) { + if (Quickshell.screens[i].name === screenName) + return Quickshell.screens[i]; + } + return Quickshell.screens.length > 0 ? Quickshell.screens[0] : null; + } + + Timer { + id: sortDebounceTimer + interval: 100 + repeat: false + onTriggered: { + _sortScheduled = false; + sortedToplevels = computeSortedToplevels(); + toplevelsChanged(); + } + } + + function scheduleSort() { + if (_sortScheduled) + return; + _sortScheduled = true; + sortDebounceTimer.restart(); + } + + Connections { + target: ToplevelManager.toplevels + function onValuesChanged() { + root.scheduleSort(); + } + } + Connections { + target: isHyprland ? Hyprland : null + enabled: isHyprland + + function onRawEvent(event) { + if (event.name === "openwindow" || event.name === "closewindow" || event.name === "movewindow" || event.name === "movewindowv2" || event.name === "workspace" || event.name === "workspacev2" || event.name === "focusedmon" || event.name === "focusedmonv2" || event.name === "activewindow" || event.name === "activewindowv2" || event.name === "changefloatingmode" || event.name === "fullscreen" || event.name === "moveintogroup" || event.name === "moveoutofgroup") { + try { + Hyprland.refreshToplevels(); + } catch (e) {} + root.scheduleSort(); + } + } + } + Connections { + target: NiriService + function onWindowsChanged() { + root.scheduleSort(); + } + } + + Component.onCompleted: { + fetchRandrData(); + detectCompositor(); + scheduleSort(); + Qt.callLater(() => { + NiriService.generateNiriLayoutConfig(); + HyprlandService.generateLayoutConfig(); + DwlService.generateLayoutConfig(); + }); + } + + Connections { + target: DwlService + function onStateChanged() { + if (isDwl && !isHyprland && !isNiri) { + scheduleSort(); + } + } + } + + function computeSortedToplevels() { + if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) + return []; + + if (useNiriSorting) + return NiriService.sortToplevels(ToplevelManager.toplevels.values); + + if (isHyprland) + return sortHyprlandToplevelsSafe(); + + return Array.from(ToplevelManager.toplevels.values); + } + + function _get(o, path, fallback) { + try { + let v = o; + for (let i = 0; i < path.length; i++) { + if (v === null || v === undefined) + return fallback; + v = v[path[i]]; + } + return (v === undefined || v === null) ? fallback : v; + } catch (e) { + return fallback; + } + } + + function sortHyprlandToplevelsSafe() { + if (!Hyprland.toplevels || !Hyprland.toplevels.values) + return []; + + const items = Array.from(Hyprland.toplevels.values); + + function _get(o, path, fb) { + try { + let v = o; + for (let k of path) { + if (v == null) + return fb; + v = v[k]; + } + return (v == null) ? fb : v; + } catch (e) { + return fb; + } + } + + let snap = []; + for (let i = 0; i < items.length; i++) { + const t = items[i]; + if (!t) + continue; + const addr = t.address || ""; + if (!addr) + continue; + const li = t.lastIpcObject || null; + + const monName = _get(li, ["monitor"], null) ?? _get(t, ["monitor", "name"], ""); + const monX = _get(t, ["monitor", "x"], Number.MAX_SAFE_INTEGER); + const monY = _get(t, ["monitor", "y"], Number.MAX_SAFE_INTEGER); + + const wsId = _get(li, ["workspace", "id"], null) ?? _get(t, ["workspace", "id"], Number.MAX_SAFE_INTEGER); + + const at = _get(li, ["at"], null); + let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : 1e9; + let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : 1e9; + + const relX = Number.isFinite(monX) ? (atX - monX) : atX; + const relY = Number.isFinite(monY) ? (atY - monY) : atY; + + snap.push({ + monKey: String(monName), + monOrderX: Number.isFinite(monX) ? monX : Number.MAX_SAFE_INTEGER, + monOrderY: Number.isFinite(monY) ? monY : Number.MAX_SAFE_INTEGER, + wsId: (typeof wsId === "number") ? wsId : Number.MAX_SAFE_INTEGER, + x: relX, + y: relY, + title: t.title || "", + address: addr, + wayland: t.wayland + }); + } + + const groups = new Map(); + for (const it of snap) { + const key = it.monKey + "::" + it.wsId; + if (!groups.has(key)) + groups.set(key, []); + groups.get(key).push(it); + } + + let groupList = []; + for (const [key, arr] of groups) { + const repr = arr[0]; + groupList.push({ + key, + monKey: repr.monKey, + monOrderX: repr.monOrderX, + monOrderY: repr.monOrderY, + wsId: repr.wsId, + items: arr + }); + } + + groupList.sort((a, b) => { + if (a.monOrderX !== b.monOrderX) + return a.monOrderX - b.monOrderX; + if (a.monOrderY !== b.monOrderY) + return a.monOrderY - b.monOrderY; + if (a.monKey !== b.monKey) + return a.monKey.localeCompare(b.monKey); + if (a.wsId !== b.wsId) + return a.wsId - b.wsId; + return 0; + }); + + const COLUMN_THRESHOLD = 48; + const JITTER_Y = 6; + + let ordered = []; + for (const g of groupList) { + const arr = g.items; + + const xs = arr.map(it => it.x).filter(x => Number.isFinite(x)).sort((a, b) => a - b); + let colCenters = []; + if (xs.length > 0) { + for (const x of xs) { + if (colCenters.length === 0) { + colCenters.push(x); + } else { + const last = colCenters[colCenters.length - 1]; + if (x - last >= COLUMN_THRESHOLD) { + colCenters.push(x); + } + } + } + } else { + colCenters = [0]; + } + + for (const it of arr) { + let bestCol = 0; + let bestDist = Number.POSITIVE_INFINITY; + for (let ci = 0; ci < colCenters.length; ci++) { + const d = Math.abs(it.x - colCenters[ci]); + if (d < bestDist) { + bestDist = d; + bestCol = ci; + } + } + it._col = bestCol; + } + + arr.sort((a, b) => { + if (a._col !== b._col) + return a._col - b._col; + + const dy = a.y - b.y; + if (Math.abs(dy) > JITTER_Y) + return dy; + + if (a.title !== b.title) + return a.title.localeCompare(b.title); + if (a.address !== b.address) + return a.address.localeCompare(b.address); + return 0; + }); + + ordered.push.apply(ordered, arr); + } + + return ordered.map(x => x.wayland).filter(w => w !== null && w !== undefined); + } + + function filterCurrentWorkspace(toplevels, screen) { + if (useNiriSorting) + return NiriService.filterCurrentWorkspace(toplevels, screen); + if (isHyprland) + return filterHyprlandCurrentWorkspaceSafe(toplevels, screen); + return toplevels; + } + + function filterCurrentDisplay(toplevels, screenName) { + if (!toplevels || toplevels.length === 0 || !screenName) + return toplevels; + if (useNiriSorting) + return NiriService.filterCurrentDisplay(toplevels, screenName); + if (isHyprland) + return filterHyprlandCurrentDisplaySafe(toplevels, screenName); + return toplevels; + } + + function filterHyprlandCurrentDisplaySafe(toplevels, screenName) { + if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) + return toplevels; + + let monitorWindows = new Set(); + try { + const hy = Array.from(Hyprland.toplevels.values); + for (const t of hy) { + const mon = _get(t, ["monitor", "name"], ""); + if (mon === screenName && t.wayland) + monitorWindows.add(t.wayland); + } + } catch (e) {} + + return toplevels.filter(w => monitorWindows.has(w)); + } + + function filterHyprlandCurrentWorkspaceSafe(toplevels, screenName) { + if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) + return toplevels; + + let currentWorkspaceId = null; + try { + if (Hyprland.monitors) { + const monitor = Hyprland.monitors.values.find(m => m.name === screenName); + if (monitor) + currentWorkspaceId = _get(monitor, ["activeWorkspace", "id"], null); + } + + if (currentWorkspaceId === null) { + const hy = Array.from(Hyprland.toplevels.values); + for (const t of hy) { + const mon = _get(t, ["monitor", "name"], ""); + const wsId = _get(t, ["workspace", "id"], null); + const active = !!_get(t, ["activated"], false); + if (mon === screenName && wsId !== null) { + if (active) { + currentWorkspaceId = wsId; + break; + } + if (currentWorkspaceId === null) + currentWorkspaceId = wsId; + } + } + } + + if (currentWorkspaceId === null && Hyprland.workspaces) { + const wss = Array.from(Hyprland.workspaces.values); + const focusedId = _get(Hyprland, ["focusedWorkspace", "id"], null); + for (const ws of wss) { + const monName = _get(ws, ["monitor", "name"], ""); + const wsId = _get(ws, ["id"], null); + if (monName === screenName && wsId !== null) { + if (focusedId !== null && wsId === focusedId) { + currentWorkspaceId = wsId; + break; + } + if (currentWorkspaceId === null) + currentWorkspaceId = wsId; + } + } + } + } catch (e) { + console.warn("CompositorService: workspace snapshot failed:", e); + } + + if (currentWorkspaceId === null) + return toplevels; + + let map = new Map(); + try { + const hy = Array.from(Hyprland.toplevels.values); + for (const t of hy) { + const wsId = _get(t, ["workspace", "id"], null); + if (t && t.wayland && wsId !== null) + map.set(t.wayland, wsId); + } + } catch (e) {} + + return toplevels.filter(w => map.get(w) === currentWorkspaceId); + } + + Timer { + id: compositorInitTimer + interval: 100 + running: true + repeat: false + onTriggered: { + detectCompositor(); + Qt.callLater(() => { + NiriService.generateNiriLayoutConfig(); + HyprlandService.generateLayoutConfig(); + DwlService.generateLayoutConfig(); + }); + } + } + + function detectCompositor() { + if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) { + isHyprland = true; + isNiri = false; + isDwl = false; + isSway = false; + isScroll = false; + isMiracle = false; + isLabwc = false; + compositor = "hyprland"; + console.info("CompositorService: Detected Hyprland"); + return; + } + + if (niriSocket && niriSocket.length > 0) { + Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => { + if (exitCode === 0) { + isNiri = true; + isHyprland = false; + isDwl = false; + isSway = false; + isScroll = false; + isMiracle = false; + isLabwc = false; + compositor = "niri"; + console.info("CompositorService: Detected Niri with socket:", niriSocket); + NiriService.generateNiriBlurrule(); + } + }, 0); + return; + } + + if (swaySocket && swaySocket.length > 0 && !scrollSocket && scrollSocket.length == 0 && !miracleSocket) { + Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => { + if (exitCode === 0) { + isNiri = false; + isHyprland = false; + isDwl = false; + isSway = true; + isScroll = false; + isMiracle = false; + isLabwc = false; + compositor = "sway"; + console.info("CompositorService: Detected Sway with socket:", swaySocket); + } + }, 0); + return; + } + + if (miracleSocket && miracleSocket.length > 0) { + Proc.runCommand("miracleSocketCheck", ["test", "-S", miracleSocket], (output, exitCode) => { + if (exitCode === 0) { + isNiri = false; + isHyprland = false; + isDwl = false; + isSway = false; + isScroll = false; + isMiracle = true; + isLabwc = false; + compositor = "miracle"; + console.info("CompositorService: Detected Miracle WM with socket:", miracleSocket); + } + }, 0); + return; + } + + if (scrollSocket && scrollSocket.length > 0 && !miracleSocket) { + Proc.runCommand("scrollSocketCheck", ["test", "-S", scrollSocket], (output, exitCode) => { + if (exitCode === 0) { + isNiri = false; + isHyprland = false; + isDwl = false; + isSway = false; + isScroll = true; + isMiracle = false; + isLabwc = false; + compositor = "scroll"; + console.info("CompositorService: Detected Scroll with socket:", scrollSocket); + } + }, 0); + return; + } + + if (labwcPid && labwcPid.length > 0) { + isHyprland = false; + isNiri = false; + isDwl = false; + isSway = false; + isScroll = false; + isMiracle = false; + isLabwc = true; + compositor = "labwc"; + console.info("CompositorService: Detected LabWC with PID:", labwcPid); + return; + } + + if (DMSService.dmsAvailable) { + Qt.callLater(checkForDwl); + } else { + isHyprland = false; + isNiri = false; + isDwl = false; + isSway = false; + isScroll = false; + isMiracle = false; + isLabwc = false; + compositor = "unknown"; + console.warn("CompositorService: No compositor detected"); + } + } + + Connections { + target: DMSService + function onCapabilitiesReceived() { + if (!isHyprland && !isNiri && !isDwl && !isLabwc) { + checkForDwl(); + } + } + } + + function checkForDwl() { + if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) { + isHyprland = false; + isNiri = false; + isDwl = true; + isSway = false; + isScroll = false; + isMiracle = false; + isLabwc = false; + compositor = "dwl"; + console.info("CompositorService: Detected DWL via DMS capability"); + } + } + + function powerOffMonitors() { + if (isNiri) + return NiriService.powerOffMonitors(); + if (isHyprland) + return Hyprland.dispatch("dpms off"); + if (isDwl) + return _dwlPowerOffMonitors(); + if (isSway || isScroll || isMiracle) { + try { + I3.dispatch("output * dpms off"); + } catch (_) {} + return; + } + if (isLabwc) { + Quickshell.execDetached(["dms", "dpms", "off"]); + } + console.warn("CompositorService: Cannot power off monitors, unknown compositor"); + } + + function powerOnMonitors() { + if (isNiri) + return NiriService.powerOnMonitors(); + if (isHyprland) + return Hyprland.dispatch("dpms on"); + if (isDwl) + return _dwlPowerOnMonitors(); + if (isSway || isScroll || isMiracle) { + try { + I3.dispatch("output * dpms on"); + } catch (_) {} + return; + } + if (isLabwc) { + Quickshell.execDetached(["dms", "dpms", "on"]); + } + console.warn("CompositorService: Cannot power on monitors, unknown compositor"); + } + + function _dwlPowerOffMonitors() { + if (!Quickshell.screens || Quickshell.screens.length === 0) { + console.warn("CompositorService: No screens available for DWL power off"); + return; + } + + for (let i = 0; i < Quickshell.screens.length; i++) { + const screen = Quickshell.screens[i]; + if (screen && screen.name) { + Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name]); + } + } + } + + function _dwlPowerOnMonitors() { + if (!Quickshell.screens || Quickshell.screens.length === 0) { + console.warn("CompositorService: No screens available for DWL power on"); + return; + } + + for (let i = 0; i < Quickshell.screens.length; i++) { + const screen = Quickshell.screens[i]; + if (screen && screen.name) { + Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name]); + } + } + } +} |